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
11 changes: 5 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
Expand Down
23 changes: 7 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
252 changes: 242 additions & 10 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -68,22 +68,48 @@ 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 {
log.Fatalf("Failed to get Git changes: %v", err)
}

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" {
Expand All @@ -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
Expand Down Expand Up @@ -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")
}
}