Skip to content

Commit a854b71

Browse files
committed
feat: enhance UI with file statistics and changes preview
- Add file statistics display showing staged, unstaged, and untracked files - Implement visual changes preview with lines added/deleted - Enhance UI with styled headers, sections, and panels using pterm - Add FileStatistics struct to track file changes - Improve commit message display with bordered box - Update spinner with custom sequence and success/failure messages - Reorder imports for better organization
1 parent 2661b9e commit a854b71

3 files changed

Lines changed: 254 additions & 32 deletions

File tree

go.mod

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ toolchain go1.24.7
66

77
require (
88
github.com/google/generative-ai-go v0.19.0
9+
github.com/openai/openai-go/v3 v3.0.1
910
github.com/pterm/pterm v0.12.80
1011
google.golang.org/api v0.223.0
1112
)
@@ -28,16 +29,14 @@ require (
2829
github.com/google/uuid v1.6.0 // indirect
2930
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
3031
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
31-
github.com/openai/openai-go/v3 v3.0.1 // indirect
32-
github.com/tidwall/gjson v1.14.4 // indirect
33-
github.com/tidwall/match v1.1.1 // indirect
34-
github.com/tidwall/pretty v1.2.1 // indirect
35-
github.com/tidwall/sjson v1.2.5 // indirect
36-
go.opencensus.io v0.24.0 // indirect
3732
github.com/gookit/color v1.5.4 // indirect
3833
github.com/lithammer/fuzzysearch v1.1.8 // indirect
3934
github.com/mattn/go-runewidth v0.0.16 // indirect
4035
github.com/rivo/uniseg v0.4.7 // indirect
36+
github.com/tidwall/gjson v1.14.4 // indirect
37+
github.com/tidwall/match v1.1.1 // indirect
38+
github.com/tidwall/pretty v1.2.1 // indirect
39+
github.com/tidwall/sjson v1.2.5 // indirect
4140
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
4241
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
4342
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect

go.sum

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT
5555
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
5656
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
5757
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
58-
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
59-
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
60-
github.com/openai/openai-go/v3 v3.0.1 h1:cub/K1g5RJwYFqgvq81/ByLHnLJ+CsdSs1QSKaVA2WA=
61-
github.com/openai/openai-go/v3 v3.0.1/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs=
6258
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
6359
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
6460
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
@@ -76,6 +72,8 @@ github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ
7672
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
7773
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
7874
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
75+
github.com/openai/openai-go/v3 v3.0.1 h1:cub/K1g5RJwYFqgvq81/ByLHnLJ+CsdSs1QSKaVA2WA=
76+
github.com/openai/openai-go/v3 v3.0.1/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs=
7977
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8078
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8179
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
@@ -93,11 +91,11 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
9391
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
9492
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
9593
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
96-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
97-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
98-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
99-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
100-
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
94+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
95+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
96+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
97+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
98+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
10199
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
102100
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
103101
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -108,13 +106,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
108106
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
109107
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
110108
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
111-
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
112-
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
113-
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
114-
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
115-
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
116-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
117-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
118109
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
119110
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
120111
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=

src/main.go

Lines changed: 242 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import (
88
"path/filepath"
99
"strings"
1010

11+
"github.com/dfanso/commit-msg/src/chatgpt"
1112
"github.com/dfanso/commit-msg/src/gemini"
1213
"github.com/dfanso/commit-msg/src/grok"
1314
"github.com/dfanso/commit-msg/src/types"
14-
"github.com/dfanso/commit-msg/src/chatgpt"
1515
"github.com/pterm/pterm"
1616
)
1717

@@ -68,22 +68,48 @@ func main() {
6868
Path: currentDir,
6969
}
7070

71+
// Get file statistics before fetching changes
72+
fileStats, err := getFileStatistics(&repoConfig)
73+
if err != nil {
74+
log.Fatalf("Failed to get file statistics: %v", err)
75+
}
76+
77+
// Display header
78+
pterm.DefaultHeader.WithFullWidth().
79+
WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)).
80+
WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)).
81+
Println("🚀 Commit Message Generator")
82+
83+
pterm.Println()
84+
85+
// Display file statistics with icons
86+
displayFileStatistics(fileStats)
87+
88+
if fileStats.TotalFiles == 0 {
89+
pterm.Warning.Println("No changes detected in the Git repository.")
90+
return
91+
}
92+
7193
// Get the changes
7294
changes, err := getGitChanges(&repoConfig)
7395
if err != nil {
7496
log.Fatalf("Failed to get Git changes: %v", err)
7597
}
7698

7799
if len(changes) == 0 {
78-
fmt.Println("No changes detected in the Git repository.")
100+
pterm.Warning.Println("No changes detected in the Git repository.")
79101
return
80102
}
81103

82-
spinnerGenerating, err := pterm.DefaultSpinner.Start("Generating commit message...")
104+
pterm.Println()
105+
106+
// Show generating spinner
107+
spinnerGenerating, err := pterm.DefaultSpinner.
108+
WithSequence("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏").
109+
Start("🤖 Generating commit message...")
83110
if err != nil {
84111
log.Fatalf("Failed to start spinner: %v", err)
85112
}
86-
defer spinnerGenerating.Stop()
87113

88114
var commitMsg string
89115
if os.Getenv("COMMIT_LLM") == "google" {
@@ -93,17 +119,23 @@ func main() {
93119
} else {
94120
commitMsg, err = grok.GenerateCommitMessage(config, changes, apiKey)
95121
}
96-
97122

98123
if err != nil {
99-
log.Fatalf("Failed to generate commit message: %v", err)
124+
spinnerGenerating.Fail("Failed to generate commit message")
125+
log.Fatalf("Error: %v", err)
100126
}
101127

102-
spinnerGenerating.Success()
128+
spinnerGenerating.Success("✅ Commit message generated successfully!")
129+
130+
pterm.Println()
103131

104-
// Display the commit message
105-
pterm.DefaultBasicText.Println()
106-
pterm.DefaultBasicText.Println(commitMsg)
132+
// Display the commit message in a styled panel
133+
displayCommitMessage(commitMsg)
134+
135+
pterm.Println()
136+
137+
// Display changes preview
138+
displayChangesPreview(fileStats)
107139
}
108140

109141
// Check if directory is a git repository
@@ -237,3 +269,203 @@ func isSmallFile(filename string) bool {
237269

238270
return info.Size() <= maxSize
239271
}
272+
273+
// FileStatistics holds statistics about changed files
274+
type FileStatistics struct {
275+
StagedFiles []string
276+
UnstagedFiles []string
277+
UntrackedFiles []string
278+
TotalFiles int
279+
LinesAdded int
280+
LinesDeleted int
281+
}
282+
283+
// Get file statistics for display
284+
func getFileStatistics(config *types.RepoConfig) (*FileStatistics, error) {
285+
stats := &FileStatistics{
286+
StagedFiles: []string{},
287+
UnstagedFiles: []string{},
288+
UntrackedFiles: []string{},
289+
}
290+
291+
// Get staged files
292+
stagedCmd := exec.Command("git", "-C", config.Path, "diff", "--name-only", "--cached")
293+
stagedOutput, err := stagedCmd.Output()
294+
if err == nil && len(stagedOutput) > 0 {
295+
stats.StagedFiles = strings.Split(strings.TrimSpace(string(stagedOutput)), "\n")
296+
}
297+
298+
// Get unstaged files
299+
unstagedCmd := exec.Command("git", "-C", config.Path, "diff", "--name-only")
300+
unstagedOutput, err := unstagedCmd.Output()
301+
if err == nil && len(unstagedOutput) > 0 {
302+
stats.UnstagedFiles = strings.Split(strings.TrimSpace(string(unstagedOutput)), "\n")
303+
}
304+
305+
// Get untracked files
306+
untrackedCmd := exec.Command("git", "-C", config.Path, "ls-files", "--others", "--exclude-standard")
307+
untrackedOutput, err := untrackedCmd.Output()
308+
if err == nil && len(untrackedOutput) > 0 {
309+
stats.UntrackedFiles = strings.Split(strings.TrimSpace(string(untrackedOutput)), "\n")
310+
}
311+
312+
// Filter empty strings
313+
stats.StagedFiles = filterEmpty(stats.StagedFiles)
314+
stats.UnstagedFiles = filterEmpty(stats.UnstagedFiles)
315+
stats.UntrackedFiles = filterEmpty(stats.UntrackedFiles)
316+
317+
stats.TotalFiles = len(stats.StagedFiles) + len(stats.UnstagedFiles) + len(stats.UntrackedFiles)
318+
319+
// Get line statistics from staged changes
320+
if len(stats.StagedFiles) > 0 {
321+
statCmd := exec.Command("git", "-C", config.Path, "diff", "--cached", "--numstat")
322+
statOutput, err := statCmd.Output()
323+
if err == nil {
324+
lines := strings.Split(strings.TrimSpace(string(statOutput)), "\n")
325+
for _, line := range lines {
326+
parts := strings.Fields(line)
327+
if len(parts) >= 2 {
328+
if added := parts[0]; added != "-" {
329+
var addedNum int
330+
fmt.Sscanf(added, "%d", &addedNum)
331+
stats.LinesAdded += addedNum
332+
}
333+
if deleted := parts[1]; deleted != "-" {
334+
var deletedNum int
335+
fmt.Sscanf(deleted, "%d", &deletedNum)
336+
stats.LinesDeleted += deletedNum
337+
}
338+
}
339+
}
340+
}
341+
}
342+
343+
return stats, nil
344+
}
345+
346+
// Filter empty strings from slice
347+
func filterEmpty(slice []string) []string {
348+
filtered := []string{}
349+
for _, s := range slice {
350+
if s != "" {
351+
filtered = append(filtered, s)
352+
}
353+
}
354+
return filtered
355+
}
356+
357+
// Display file statistics with colored output
358+
func displayFileStatistics(stats *FileStatistics) {
359+
pterm.DefaultSection.Println("📊 Changes Summary")
360+
361+
// Create bullet list items
362+
bulletItems := []pterm.BulletListItem{}
363+
364+
if len(stats.StagedFiles) > 0 {
365+
bulletItems = append(bulletItems, pterm.BulletListItem{
366+
Level: 0,
367+
Text: pterm.Green(fmt.Sprintf("✅ Staged files: %d", len(stats.StagedFiles))),
368+
TextStyle: pterm.NewStyle(pterm.FgGreen),
369+
BulletStyle: pterm.NewStyle(pterm.FgGreen),
370+
})
371+
for i, file := range stats.StagedFiles {
372+
if i < 5 { // Show first 5 files
373+
bulletItems = append(bulletItems, pterm.BulletListItem{
374+
Level: 1,
375+
Text: file,
376+
})
377+
}
378+
}
379+
if len(stats.StagedFiles) > 5 {
380+
bulletItems = append(bulletItems, pterm.BulletListItem{
381+
Level: 1,
382+
Text: pterm.Gray(fmt.Sprintf("... and %d more", len(stats.StagedFiles)-5)),
383+
})
384+
}
385+
}
386+
387+
if len(stats.UnstagedFiles) > 0 {
388+
bulletItems = append(bulletItems, pterm.BulletListItem{
389+
Level: 0,
390+
Text: pterm.Yellow(fmt.Sprintf("⚠️ Unstaged files: %d", len(stats.UnstagedFiles))),
391+
TextStyle: pterm.NewStyle(pterm.FgYellow),
392+
BulletStyle: pterm.NewStyle(pterm.FgYellow),
393+
})
394+
for i, file := range stats.UnstagedFiles {
395+
if i < 3 {
396+
bulletItems = append(bulletItems, pterm.BulletListItem{
397+
Level: 1,
398+
Text: file,
399+
})
400+
}
401+
}
402+
if len(stats.UnstagedFiles) > 3 {
403+
bulletItems = append(bulletItems, pterm.BulletListItem{
404+
Level: 1,
405+
Text: pterm.Gray(fmt.Sprintf("... and %d more", len(stats.UnstagedFiles)-3)),
406+
})
407+
}
408+
}
409+
410+
if len(stats.UntrackedFiles) > 0 {
411+
bulletItems = append(bulletItems, pterm.BulletListItem{
412+
Level: 0,
413+
Text: pterm.Cyan(fmt.Sprintf("📝 Untracked files: %d", len(stats.UntrackedFiles))),
414+
TextStyle: pterm.NewStyle(pterm.FgCyan),
415+
BulletStyle: pterm.NewStyle(pterm.FgCyan),
416+
})
417+
for i, file := range stats.UntrackedFiles {
418+
if i < 3 {
419+
bulletItems = append(bulletItems, pterm.BulletListItem{
420+
Level: 1,
421+
Text: file,
422+
})
423+
}
424+
}
425+
if len(stats.UntrackedFiles) > 3 {
426+
bulletItems = append(bulletItems, pterm.BulletListItem{
427+
Level: 1,
428+
Text: pterm.Gray(fmt.Sprintf("... and %d more", len(stats.UntrackedFiles)-3)),
429+
})
430+
}
431+
}
432+
433+
pterm.DefaultBulletList.WithItems(bulletItems).Render()
434+
}
435+
436+
// Display commit message in a styled panel
437+
func displayCommitMessage(message string) {
438+
pterm.DefaultSection.Println("📝 Generated Commit Message")
439+
440+
// Create a panel with the commit message
441+
panel := pterm.DefaultBox.
442+
WithTitle("Commit Message").
443+
WithTitleTopCenter().
444+
WithBoxStyle(pterm.NewStyle(pterm.FgLightGreen)).
445+
WithHorizontalString("─").
446+
WithVerticalString("│").
447+
WithTopLeftCornerString("┌").
448+
WithTopRightCornerString("┐").
449+
WithBottomLeftCornerString("└").
450+
WithBottomRightCornerString("┘")
451+
452+
panel.Println(pterm.LightGreen(message))
453+
}
454+
455+
// Display changes preview
456+
func displayChangesPreview(stats *FileStatistics) {
457+
pterm.DefaultSection.Println("🔍 Changes Preview")
458+
459+
// Create info boxes
460+
if stats.LinesAdded > 0 || stats.LinesDeleted > 0 {
461+
infoData := [][]string{
462+
{"Lines Added", pterm.Green(fmt.Sprintf("+%d", stats.LinesAdded))},
463+
{"Lines Deleted", pterm.Red(fmt.Sprintf("-%d", stats.LinesDeleted))},
464+
{"Total Files", pterm.Cyan(fmt.Sprintf("%d", stats.TotalFiles))},
465+
}
466+
467+
pterm.DefaultTable.WithHasHeader(false).WithData(infoData).Render()
468+
} else {
469+
pterm.Info.Println("No line statistics available for unstaged changes")
470+
}
471+
}

0 commit comments

Comments
 (0)