diff --git a/go.mod b/go.mod index 7bba9cb..8a60785 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.24.7 require ( github.com/google/generative-ai-go v0.19.0 + github.com/openai/openai-go/v3 v3.0.1 github.com/pterm/pterm v0.12.80 google.golang.org/api v0.223.0 ) @@ -28,16 +29,14 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/openai/openai-go/v3 v3.0.1 // indirect - github.com/tidwall/gjson v1.14.4 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect - go.opencensus.io v0.24.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect diff --git a/go.sum b/go.sum index c4435ed..4765418 100644 --- a/go.sum +++ b/go.sum @@ -55,10 +55,6 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/openai/openai-go/v3 v3.0.1 h1:cub/K1g5RJwYFqgvq81/ByLHnLJ+CsdSs1QSKaVA2WA= -github.com/openai/openai-go/v3 v3.0.1/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= 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 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/openai/openai-go/v3 v3.0.1 h1:cub/K1g5RJwYFqgvq81/ByLHnLJ+CsdSs1QSKaVA2WA= +github.com/openai/openai-go/v3 v3.0.1/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= 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= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= diff --git a/src/main.go b/src/main.go index 65fac9c..0c98d1d 100644 --- a/src/main.go +++ b/src/main.go @@ -8,10 +8,10 @@ import ( "path/filepath" "strings" + "github.com/dfanso/commit-msg/src/chatgpt" "github.com/dfanso/commit-msg/src/gemini" "github.com/dfanso/commit-msg/src/grok" "github.com/dfanso/commit-msg/src/types" - "github.com/dfanso/commit-msg/src/chatgpt" "github.com/pterm/pterm" ) @@ -68,6 +68,28 @@ func main() { Path: currentDir, } + // Get file statistics before fetching changes + fileStats, err := getFileStatistics(&repoConfig) + if err != nil { + log.Fatalf("Failed to get file statistics: %v", err) + } + + // Display header + pterm.DefaultHeader.WithFullWidth(). + WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)). + WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)). + Println("🚀 Commit Message Generator") + + pterm.Println() + + // Display file statistics with icons + displayFileStatistics(fileStats) + + if fileStats.TotalFiles == 0 { + pterm.Warning.Println("No changes detected in the Git repository.") + return + } + // Get the changes changes, err := getGitChanges(&repoConfig) if err != nil { @@ -75,15 +97,19 @@ func main() { } if len(changes) == 0 { - fmt.Println("No changes detected in the Git repository.") + pterm.Warning.Println("No changes detected in the Git repository.") return } - spinnerGenerating, err := pterm.DefaultSpinner.Start("Generating commit message...") + pterm.Println() + + // Show generating spinner + spinnerGenerating, err := pterm.DefaultSpinner. + WithSequence("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"). + Start("🤖 Generating commit message...") if err != nil { log.Fatalf("Failed to start spinner: %v", err) } - defer spinnerGenerating.Stop() var commitMsg string if os.Getenv("COMMIT_LLM") == "google" { @@ -93,17 +119,23 @@ func main() { } else { commitMsg, err = grok.GenerateCommitMessage(config, changes, apiKey) } - if err != nil { - log.Fatalf("Failed to generate commit message: %v", err) + spinnerGenerating.Fail("Failed to generate commit message") + log.Fatalf("Error: %v", err) } - spinnerGenerating.Success() + spinnerGenerating.Success("✅ Commit message generated successfully!") + + pterm.Println() - // Display the commit message - pterm.DefaultBasicText.Println() - pterm.DefaultBasicText.Println(commitMsg) + // Display the commit message in a styled panel + displayCommitMessage(commitMsg) + + pterm.Println() + + // Display changes preview + displayChangesPreview(fileStats) } // Check if directory is a git repository @@ -237,3 +269,203 @@ func isSmallFile(filename string) bool { return info.Size() <= maxSize } + +// FileStatistics holds statistics about changed files +type FileStatistics struct { + StagedFiles []string + UnstagedFiles []string + UntrackedFiles []string + TotalFiles int + LinesAdded int + LinesDeleted int +} + +// Get file statistics for display +func getFileStatistics(config *types.RepoConfig) (*FileStatistics, error) { + stats := &FileStatistics{ + StagedFiles: []string{}, + UnstagedFiles: []string{}, + UntrackedFiles: []string{}, + } + + // Get staged files + stagedCmd := exec.Command("git", "-C", config.Path, "diff", "--name-only", "--cached") + stagedOutput, err := stagedCmd.Output() + if err == nil && len(stagedOutput) > 0 { + stats.StagedFiles = strings.Split(strings.TrimSpace(string(stagedOutput)), "\n") + } + + // Get unstaged files + unstagedCmd := exec.Command("git", "-C", config.Path, "diff", "--name-only") + unstagedOutput, err := unstagedCmd.Output() + if err == nil && len(unstagedOutput) > 0 { + stats.UnstagedFiles = strings.Split(strings.TrimSpace(string(unstagedOutput)), "\n") + } + + // Get untracked files + untrackedCmd := exec.Command("git", "-C", config.Path, "ls-files", "--others", "--exclude-standard") + untrackedOutput, err := untrackedCmd.Output() + if err == nil && len(untrackedOutput) > 0 { + stats.UntrackedFiles = strings.Split(strings.TrimSpace(string(untrackedOutput)), "\n") + } + + // Filter empty strings + stats.StagedFiles = filterEmpty(stats.StagedFiles) + stats.UnstagedFiles = filterEmpty(stats.UnstagedFiles) + stats.UntrackedFiles = filterEmpty(stats.UntrackedFiles) + + stats.TotalFiles = len(stats.StagedFiles) + len(stats.UnstagedFiles) + len(stats.UntrackedFiles) + + // Get line statistics from staged changes + if len(stats.StagedFiles) > 0 { + statCmd := exec.Command("git", "-C", config.Path, "diff", "--cached", "--numstat") + statOutput, err := statCmd.Output() + if err == nil { + lines := strings.Split(strings.TrimSpace(string(statOutput)), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) >= 2 { + if added := parts[0]; added != "-" { + var addedNum int + fmt.Sscanf(added, "%d", &addedNum) + stats.LinesAdded += addedNum + } + if deleted := parts[1]; deleted != "-" { + var deletedNum int + fmt.Sscanf(deleted, "%d", &deletedNum) + stats.LinesDeleted += deletedNum + } + } + } + } + } + + return stats, nil +} + +// Filter empty strings from slice +func filterEmpty(slice []string) []string { + filtered := []string{} + for _, s := range slice { + if s != "" { + filtered = append(filtered, s) + } + } + return filtered +} + +// Display file statistics with colored output +func displayFileStatistics(stats *FileStatistics) { + pterm.DefaultSection.Println("📊 Changes Summary") + + // Create bullet list items + bulletItems := []pterm.BulletListItem{} + + if len(stats.StagedFiles) > 0 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 0, + Text: pterm.Green(fmt.Sprintf("✅ Staged files: %d", len(stats.StagedFiles))), + TextStyle: pterm.NewStyle(pterm.FgGreen), + BulletStyle: pterm.NewStyle(pterm.FgGreen), + }) + for i, file := range stats.StagedFiles { + if i < 5 { // Show first 5 files + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 1, + Text: file, + }) + } + } + if len(stats.StagedFiles) > 5 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 1, + Text: pterm.Gray(fmt.Sprintf("... and %d more", len(stats.StagedFiles)-5)), + }) + } + } + + if len(stats.UnstagedFiles) > 0 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 0, + Text: pterm.Yellow(fmt.Sprintf("⚠️ Unstaged files: %d", len(stats.UnstagedFiles))), + TextStyle: pterm.NewStyle(pterm.FgYellow), + BulletStyle: pterm.NewStyle(pterm.FgYellow), + }) + for i, file := range stats.UnstagedFiles { + if i < 3 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 1, + Text: file, + }) + } + } + if len(stats.UnstagedFiles) > 3 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 1, + Text: pterm.Gray(fmt.Sprintf("... and %d more", len(stats.UnstagedFiles)-3)), + }) + } + } + + if len(stats.UntrackedFiles) > 0 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 0, + Text: pterm.Cyan(fmt.Sprintf("📝 Untracked files: %d", len(stats.UntrackedFiles))), + TextStyle: pterm.NewStyle(pterm.FgCyan), + BulletStyle: pterm.NewStyle(pterm.FgCyan), + }) + for i, file := range stats.UntrackedFiles { + if i < 3 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 1, + Text: file, + }) + } + } + if len(stats.UntrackedFiles) > 3 { + bulletItems = append(bulletItems, pterm.BulletListItem{ + Level: 1, + Text: pterm.Gray(fmt.Sprintf("... and %d more", len(stats.UntrackedFiles)-3)), + }) + } + } + + pterm.DefaultBulletList.WithItems(bulletItems).Render() +} + +// Display commit message in a styled panel +func displayCommitMessage(message string) { + pterm.DefaultSection.Println("📝 Generated Commit Message") + + // Create a panel with the commit message + panel := pterm.DefaultBox. + WithTitle("Commit Message"). + WithTitleTopCenter(). + WithBoxStyle(pterm.NewStyle(pterm.FgLightGreen)). + WithHorizontalString("─"). + WithVerticalString("│"). + WithTopLeftCornerString("┌"). + WithTopRightCornerString("┐"). + WithBottomLeftCornerString("└"). + WithBottomRightCornerString("┘") + + panel.Println(pterm.LightGreen(message)) +} + +// Display changes preview +func displayChangesPreview(stats *FileStatistics) { + pterm.DefaultSection.Println("🔍 Changes Preview") + + // Create info boxes + if stats.LinesAdded > 0 || stats.LinesDeleted > 0 { + infoData := [][]string{ + {"Lines Added", pterm.Green(fmt.Sprintf("+%d", stats.LinesAdded))}, + {"Lines Deleted", pterm.Red(fmt.Sprintf("-%d", stats.LinesDeleted))}, + {"Total Files", pterm.Cyan(fmt.Sprintf("%d", stats.TotalFiles))}, + } + + pterm.DefaultTable.WithHasHeader(false).WithData(infoData).Render() + } else { + pterm.Info.Println("No line statistics available for unstaged changes") + } +}