From bd5d729c38988cca972d8816b860a8afcc48e227 Mon Sep 17 00:00:00 2001 From: andev0x Date: Sun, 24 May 2026 00:43:22 +0700 Subject: [PATCH 1/2] feat(formatter): enhance line wrapping and AI context refinement - Implement structural line detection (lists and indentation) in Formatter. - Add subject-to-body overflow with proper blank line separation. - Enrich AI prompt with recent commit history for style reference. - Refine diff summarization logic to include hunk headers and line limits. - Update unit tests for new formatting and prompt rendering behavior. --- GEMINI.md | 25 ++++ assets/prompts/system_prompt.txt | 4 + internal/ai/ai_test.go | 1 + internal/ai/prompt.go | 8 +- internal/analyzer/analyzer.go | 9 +- internal/formatter/formatter.go | 175 ++++++++++++++++++++++++--- internal/formatter/formatter_test.go | 28 +++++ 7 files changed, 231 insertions(+), 19 deletions(-) create mode 100644 GEMINI.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..bd87ee2 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,25 @@ +# Gitmit Project Conventions + +## Commit Messages +We follow a professional Conventional Commits structure with detailed bodies for complex changes. + +### Structure +- **Subject:** Concise `type(scope): description` (aim for ~50 chars). +- **Body:** A blank line followed by a bulleted list of changes. Each bullet should be concise and professional. + +### Example +```text +feat(formatter): implement line-length constraints and enhanced AI context + + - Add maxSubjectLength and maxBodyLength configuration options. + - Update Formatter to support automatic line wrapping and subject-to-body + overflow with blank line separation. + - Enrich AI prompt with summarized git diff content for better commit + body generation. + - Refine wrapping logic to preserve multi-paragraph structures. + - Add unit tests for the new formatting and wrapping behavior. +``` + +## Workflow +When suggesting commit messages, always analyze the full context using: +`git status && git diff HEAD && git log -n 5 --oneline` diff --git a/assets/prompts/system_prompt.txt b/assets/prompts/system_prompt.txt index d555461..3b3202e 100644 --- a/assets/prompts/system_prompt.txt +++ b/assets/prompts/system_prompt.txt @@ -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}} diff --git a/internal/ai/ai_test.go b/internal/ai/ai_test.go index ac25679..4503131 100644 --- a/internal/ai/ai_test.go +++ b/internal/ai/ai_test.go @@ -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 { diff --git a/internal/ai/prompt.go b/internal/ai/prompt.go index b0ccb79..8408310 100644 --- a/internal/ai/prompt.go +++ b/internal/ai/prompt.go @@ -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 @@ -20,6 +21,7 @@ type PromptContext struct { DependencyAlert string DiffSummary DiffSummary DiffContent string + RecentCommits []string } // DiffSummary contains ratio of changes @@ -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, @@ -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 diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 03b5569..1d02435 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -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 @@ -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 } } diff --git a/internal/formatter/formatter.go b/internal/formatter/formatter.go index 06e1ebb..29c088e 100644 --- a/internal/formatter/formatter.go +++ b/internal/formatter/formatter.go @@ -2,6 +2,7 @@ package formatter import ( "fmt" + "regexp" "strings" ) @@ -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 @@ -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 { @@ -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 @@ -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() +} diff --git a/internal/formatter/formatter_test.go b/internal/formatter/formatter_test.go index 4203024..ecae217 100644 --- a/internal/formatter/formatter_test.go +++ b/internal/formatter/formatter_test.go @@ -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", From 9bad32f6f3eb75dba46dc37a41e168de8a799e8a Mon Sep 17 00:00:00 2001 From: andev0x Date: Sun, 24 May 2026 00:51:09 +0700 Subject: [PATCH 2/2] chore(optimizer/func): refactor diff summarization logic with improved AI context --- GEMINI.md | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 GEMINI.md diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index bd87ee2..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,25 +0,0 @@ -# Gitmit Project Conventions - -## Commit Messages -We follow a professional Conventional Commits structure with detailed bodies for complex changes. - -### Structure -- **Subject:** Concise `type(scope): description` (aim for ~50 chars). -- **Body:** A blank line followed by a bulleted list of changes. Each bullet should be concise and professional. - -### Example -```text -feat(formatter): implement line-length constraints and enhanced AI context - - - Add maxSubjectLength and maxBodyLength configuration options. - - Update Formatter to support automatic line wrapping and subject-to-body - overflow with blank line separation. - - Enrich AI prompt with summarized git diff content for better commit - body generation. - - Refine wrapping logic to preserve multi-paragraph structures. - - Add unit tests for the new formatting and wrapping behavior. -``` - -## Workflow -When suggesting commit messages, always analyze the full context using: -`git status && git diff HEAD && git log -n 5 --oneline`