Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/prompts/system_prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Metadata Context:
- Dependency Changes: {{.DependencyAlert}}
- Added/Deleted Line Ratio: {{printf "%.2f" .DiffSummary.Ratio}}

Recent Commit History (for style reference):
{{range .RecentCommits}}- {{.}}
{{end}}

Summarized Git Diff:
{{.DiffContent}}

Expand Down
1 change: 1 addition & 0 deletions internal/ai/ai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestRenderPrompt(t *testing.T) {
"internal/auth/login.go",
"[func] Login",
"Added/Deleted Line Ratio: 0.83",
"Recent Commit History",
}

for _, part := range expectedParts {
Expand Down
8 changes: 7 additions & 1 deletion internal/ai/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/andev0x/gitmit/assets"
"github.com/andev0x/gitmit/internal/analyzer"
"github.com/andev0x/gitmit/internal/history"
)

// PromptContext represents the data structure passed to the prompt template
Expand All @@ -20,6 +21,7 @@ type PromptContext struct {
DependencyAlert string
DiffSummary DiffSummary
DiffContent string
RecentCommits []string
}

// DiffSummary contains ratio of changes
Expand Down Expand Up @@ -61,6 +63,9 @@ func RenderPrompt(msg *analyzer.CommitMessage, projectType, branchName string) (
ratio = float64(msg.TotalAdded) / float64(total)
}

// Fetch recent commits for style reference
recentCommits, _ := history.GetRecentCommits(5)

ctx := PromptContext{
ProjectType: projectType,
CurrentBranch: branchName,
Expand All @@ -71,7 +76,8 @@ func RenderPrompt(msg *analyzer.CommitMessage, projectType, branchName string) (
DiffSummary: DiffSummary{
Ratio: ratio,
},
DiffContent: msg.FullDiff,
DiffContent: msg.FullDiff,
RecentCommits: recentCommits,
}

var buf bytes.Buffer
Expand Down
9 changes: 7 additions & 2 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1290,11 +1290,16 @@ func (a *Analyzer) summarizeDiff(diff string) string {
var summary strings.Builder
scanner := bufio.NewScanner(strings.NewReader(diff))
lineCount := 0
maxLines := 20 // Limit lines per file to avoid context bloat
maxLines := 25 // Limit lines per file to avoid context bloat

for scanner.Scan() {
line := scanner.Text()
// Only include added/removed lines and hunk headers
// Skip binary files or extremely long lines
if len(line) > 500 {
continue
}

if strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "@@") {
if strings.HasPrefix(line, "+++") || strings.HasPrefix(line, "---") {
continue
Expand All @@ -1304,7 +1309,7 @@ func (a *Analyzer) summarizeDiff(diff string) string {
lineCount++
}
if lineCount >= maxLines {
summary.WriteString("... (truncated)\n")
summary.WriteString("... (rest of file truncated)\n")
break
}
}
Expand Down
175 changes: 159 additions & 16 deletions internal/formatter/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package formatter

import (
"fmt"
"regexp"
"strings"
)

Expand Down Expand Up @@ -30,7 +31,8 @@ func (f *Formatter) FormatMessage(msg string, isMajor bool) string {
subject := strings.TrimSpace(parts[0])
body := ""
if len(parts) > 1 {
body = strings.TrimSpace(parts[1])
body = strings.TrimLeft(parts[1], "\n\r")
body = strings.TrimRight(body, "\n\r\t ")
}

// Remove redundant phrases from subject
Expand All @@ -49,6 +51,7 @@ func (f *Formatter) FormatMessage(msg string, isMajor bool) string {
subjectParts := strings.SplitN(wrapped, "\n", 2)
subject = subjectParts[0]
if len(subjectParts) > 1 {
// Subject overflow becomes the start of the body
if body != "" {
body = subjectParts[1] + "\n\n" + body
} else {
Expand All @@ -68,7 +71,7 @@ func (f *Formatter) FormatMessage(msg string, isMajor bool) string {
return subject
}

// wrapString wraps a string at the specified limit, preserving paragraphs
// wrapString wraps a string at the specified limit, preserving paragraphs and structures
func (f *Formatter) wrapString(s string, limit int) string {
if limit <= 0 {
return s
Expand All @@ -82,27 +85,167 @@ func (f *Formatter) wrapString(s string, limit int) string {
result.WriteString("\n\n")
}

words := strings.Fields(p)
if len(words) == 0 {
continue
}
lines := strings.Split(p, "\n")
var currentParagraph strings.Builder

currentLineLength := 0
for j, word := range words {
if j > 0 {
if currentLineLength+1+len(word) > limit {
for _, line := range lines {
if f.isStructural(line) {
// Flush any pending paragraph text
if currentParagraph.Len() > 0 {
result.WriteString(f.reflow(currentParagraph.String(), limit))
result.WriteString("\n")
currentLineLength = 0
} else {
result.WriteString(" ")
currentLineLength++
currentParagraph.Reset()
}
// Wrap the structural line itself (preserving its prefix if possible)
result.WriteString(f.wrapLine(line, limit))
result.WriteString("\n")
} else {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if currentParagraph.Len() > 0 {
currentParagraph.WriteString(" ")
}
currentParagraph.WriteString(trimmed)
}
}

if currentParagraph.Len() > 0 {
result.WriteString(f.reflow(currentParagraph.String(), limit))
}

result.WriteString(word)
currentLineLength += len(word)
// Cleanup trailing newline from structural lines at the end of a paragraph
resStr := result.String()
if strings.HasSuffix(resStr, "\n") {
result.Reset()
result.WriteString(strings.TrimSuffix(resStr, "\n"))
}
}

return result.String()
}

// isStructural identifies lines that should not be reflowed into paragraphs
func (f *Formatter) isStructural(line string) bool {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
return false
}

// List markers at the start of the trimmed line
markers := []string{"- ", "* ", "+ "}
for _, m := range markers {
if strings.HasPrefix(trimmed, m) {
return true
}
}

// Numeric list markers (e.g., "1. ")
numericRegex := regexp.MustCompile(`^\d+\.\s`)
if numericRegex.MatchString(trimmed) {
return true
}

// Significant indentation (at least 2 spaces or a tab)
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
return true
}

return false
}

// reflow joins words and wraps them at the limit
func (f *Formatter) reflow(s string, limit int) string {
words := strings.Fields(s)
if len(words) == 0 {
return ""
}

var res strings.Builder
currLen := 0
for i, w := range words {
if i > 0 {
if currLen+1+len(w) > limit {
res.WriteString("\n")
currLen = 0
} else {
res.WriteString(" ")
currLen++
}
}
res.WriteString(w)
currLen += len(w)
}
return res.String()
}

// wrapLine wraps a single structural line, attempting to preserve indentation
func (f *Formatter) wrapLine(line string, limit int) string {
if len(line) <= limit {
return line
}

// Find indentation and prefix
indent := ""
for _, char := range line {
if char == ' ' || char == '\t' {
indent += string(char)
} else {
break
}
}

// Also check for list markers
content := line[len(indent):]
prefix := ""
markers := []string{"- ", "* ", "+ "}
for _, m := range markers {
if strings.HasPrefix(content, m) {
prefix = m
content = content[len(m):]
break
}
}

// Handle numeric markers
numericRegex := regexp.MustCompile(`^\d+\.\s`)
if loc := numericRegex.FindStringIndex(content); loc != nil && loc[0] == 0 {
prefix = content[loc[0]:loc[1]]
content = content[loc[1]:]
}

words := strings.Fields(content)
if len(words) == 0 {
return line
}

var res strings.Builder
res.WriteString(indent)
res.WriteString(prefix)
currLen := len(indent) + len(prefix)

for i, w := range words {
if i > 0 {
if currLen+1+len(w) > limit {
res.WriteString("\n")
res.WriteString(indent)
// Extra indentation for wrapped list items
if prefix != "" {
res.WriteString(" ")
}
currLen = len(indent)
if prefix != "" {
currLen += 2
}
} else {
res.WriteString(" ")
currLen++
}
}
res.WriteString(w)
currLen += len(w)
}

return res.String()
}
28 changes: 28 additions & 0 deletions internal/formatter/formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ func TestFormatMessage(t *testing.T) {
maxBody: 10,
expected: "feat: add\n\nfeature\n\nThis is a\nbody\nmessage\nthat is\nvery long.",
},
{
name: "preserve list structure",
msg: "feat: add feature\n\n- Item 1\n- Item 2 which is quite long",
maxSubject: 50,
maxBody: 15,
expected: "feat: add feature\n\n- Item 1\n- Item 2 which\n is quite long",
},
{
name: "preserve indentation",
msg: "feat: add feature\n\n Indented text that should not be joined",
maxSubject: 50,
maxBody: 20,
expected: "feat: add feature\n\n Indented text\n that should not\n be joined",
},
{
name: "multi-paragraph reflow",
msg: "feat: add feature\n\nParagraph 1 line 1\nline 2\n\nParagraph 2",
maxSubject: 50,
maxBody: 50,
expected: "feat: add feature\n\nParagraph 1 line 1 line 2\n\nParagraph 2",
},
{
name: "subject overflow to body with blank line",
msg: "feat: this is a very long subject line that will overflow",
maxSubject: 20,
maxBody: 72,
expected: "feat: this is a very\n\nlong subject line that will overflow",
},
{
name: "redundant phrases",
msg: "feat feat: add add new feature",
Expand Down
Loading