diff --git a/README.md b/README.md index a04dc99..818e03e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Hacktoberfest](https://img.shields.io/badge/Hacktoberfest-2025-orange.svg)](https://hacktoberfest.com/) [![Go Version](https://img.shields.io/badge/Go-1.23.4-blue.svg)](https://golang.org/) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) - +![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/DFanso/commit-msg?utm_source=oss&utm_medium=github&utm_campaign=DFanso%2Fcommit-msg&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) `commit-msg` is a command-line tool that generates commit messages using LLM (Large Language Models). It is designed to help developers create clear and concise commit messages for their Git repositories automatically by analyzing your staged changes. ## Screenshot diff --git a/src/internal/display/display.go b/src/internal/display/display.go new file mode 100644 index 0000000..0ad28c7 --- /dev/null +++ b/src/internal/display/display.go @@ -0,0 +1,133 @@ +package display + +import ( + "fmt" + + "github.com/pterm/pterm" +) + +// FileStatistics holds statistics about changed files +type FileStatistics struct { + StagedFiles []string + UnstagedFiles []string + UntrackedFiles []string + TotalFiles int + LinesAdded int + LinesDeleted int +} + +// ShowFileStatistics displays file statistics with colored output +func ShowFileStatistics(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() +} + +// ShowCommitMessage displays the commit message in a styled panel +func ShowCommitMessage(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)) +} + +// ShowChangesPreview displays a preview of changes with line statistics +func ShowChangesPreview(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") + } +} diff --git a/src/internal/git/operations.go b/src/internal/git/operations.go new file mode 100644 index 0000000..46185ae --- /dev/null +++ b/src/internal/git/operations.go @@ -0,0 +1,119 @@ +package git + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/dfanso/commit-msg/src/internal/utils" + "github.com/dfanso/commit-msg/src/types" +) + +// IsRepository checks if a directory is a git repository +func IsRepository(path string) bool { + cmd := exec.Command("git", "-C", path, "rev-parse", "--is-inside-work-tree") + output, err := cmd.CombinedOutput() + if err != nil { + return false + } + return strings.TrimSpace(string(output)) == "true" +} + +// GetChanges retrieves all Git changes including staged, unstaged, and untracked files +func GetChanges(config *types.RepoConfig) (string, error) { + var changes strings.Builder + + // 1. Check for unstaged changes + cmd := exec.Command("git", "-C", config.Path, "diff", "--name-status") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("git diff failed: %v", err) + } + + if len(output) > 0 { + changes.WriteString("Unstaged changes:\n") + changes.WriteString(string(output)) + changes.WriteString("\n\n") + + // Get the content of these changes + diffCmd := exec.Command("git", "-C", config.Path, "diff") + diffOutput, err := diffCmd.Output() + if err != nil { + return "", fmt.Errorf("git diff content failed: %v", err) + } + + changes.WriteString("Unstaged diff content:\n") + changes.WriteString(string(diffOutput)) + changes.WriteString("\n\n") + } + + // 2. Check for staged changes + stagedCmd := exec.Command("git", "-C", config.Path, "diff", "--name-status", "--cached") + stagedOutput, err := stagedCmd.Output() + if err != nil { + return "", fmt.Errorf("git diff --cached failed: %v", err) + } + + if len(stagedOutput) > 0 { + changes.WriteString("Staged changes:\n") + changes.WriteString(string(stagedOutput)) + changes.WriteString("\n\n") + + // Get the content of these changes + stagedDiffCmd := exec.Command("git", "-C", config.Path, "diff", "--cached") + stagedDiffOutput, err := stagedDiffCmd.Output() + if err != nil { + return "", fmt.Errorf("git diff --cached content failed: %v", err) + } + + changes.WriteString("Staged diff content:\n") + changes.WriteString(string(stagedDiffOutput)) + changes.WriteString("\n\n") + } + + // 3. Check for untracked files + untrackedCmd := exec.Command("git", "-C", config.Path, "ls-files", "--others", "--exclude-standard") + untrackedOutput, err := untrackedCmd.Output() + if err != nil { + return "", fmt.Errorf("git ls-files failed: %v", err) + } + + if len(untrackedOutput) > 0 { + changes.WriteString("Untracked files:\n") + changes.WriteString(string(untrackedOutput)) + changes.WriteString("\n\n") + + // Try to get content of untracked files (limited to text files and smaller size) + untrackedFiles := strings.Split(strings.TrimSpace(string(untrackedOutput)), "\n") + for _, file := range untrackedFiles { + if file == "" { + continue + } + + fullPath := filepath.Join(config.Path, file) + if utils.IsTextFile(fullPath) && utils.IsSmallFile(fullPath) { + fileContent, err := os.ReadFile(fullPath) + if err != nil { + // Log but don't fail - untracked file may have been deleted or is inaccessible + continue + } + changes.WriteString(fmt.Sprintf("Content of new file %s:\n", file)) + changes.WriteString(string(fileContent)) + changes.WriteString("\n\n") + } + } + } + + // 4. Get recent commits for context + recentCommitsCmd := exec.Command("git", "-C", config.Path, "log", "--oneline", "-n", "3") + recentCommitsOutput, err := recentCommitsCmd.Output() + if err == nil && len(recentCommitsOutput) > 0 { + changes.WriteString("Recent commits for context:\n") + changes.WriteString(string(recentCommitsOutput)) + changes.WriteString("\n") + } + + return changes.String(), nil +} diff --git a/src/internal/stats/statistics.go b/src/internal/stats/statistics.go new file mode 100644 index 0000000..d579345 --- /dev/null +++ b/src/internal/stats/statistics.go @@ -0,0 +1,74 @@ +package stats + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/dfanso/commit-msg/src/internal/display" + "github.com/dfanso/commit-msg/src/internal/utils" + "github.com/dfanso/commit-msg/src/types" +) + +// GetFileStatistics collects comprehensive file statistics from Git +func GetFileStatistics(config *types.RepoConfig) (*display.FileStatistics, error) { + stats := &display.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 = utils.FilterEmpty(stats.StagedFiles) + stats.UnstagedFiles = utils.FilterEmpty(stats.UnstagedFiles) + stats.UntrackedFiles = utils.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 +} diff --git a/src/internal/utils/utils.go b/src/internal/utils/utils.go new file mode 100644 index 0000000..bc485fb --- /dev/null +++ b/src/internal/utils/utils.go @@ -0,0 +1,58 @@ +package utils + +import ( + "os" + "path/filepath" + "strings" +) + +// NormalizePath handles both forward and backslashes +func NormalizePath(path string) string { + // Replace backslashes with forward slashes + normalized := strings.ReplaceAll(path, "\\", "/") + // Remove any trailing slash + normalized = strings.TrimSuffix(normalized, "/") + return normalized +} + +// IsTextFile checks if a file is likely to be a text file +func IsTextFile(filename string) bool { + // List of common text file extensions + textExtensions := []string{ + ".txt", ".md", ".go", ".js", ".py", ".java", ".c", ".cpp", ".h", + ".html", ".css", ".json", ".xml", ".yaml", ".yml", ".sh", ".bash", + ".ts", ".tsx", ".jsx", ".php", ".rb", ".rs", ".dart", + } + + ext := strings.ToLower(filepath.Ext(filename)) + for _, textExt := range textExtensions { + if ext == textExt { + return true + } + } + + return false +} + +// IsSmallFile checks if a file is small enough to include in context +func IsSmallFile(filename string) bool { + const maxSize = 10 * 1024 // 10KB max + + info, err := os.Stat(filename) + if err != nil { + return false + } + + return info.Size() <= maxSize +} + +// FilterEmpty removes empty strings from a slice +func FilterEmpty(slice []string) []string { + filtered := []string{} + for _, s := range slice { + if s != "" { + filtered = append(filtered, s) + } + } + return filtered +} diff --git a/src/main.go b/src/main.go index 999fbb4..caa3fad 100644 --- a/src/main.go +++ b/src/main.go @@ -1,31 +1,21 @@ package main import ( - "fmt" "log" "os" - "os/exec" - "path/filepath" - "strings" "github.com/atotto/clipboard" "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/internal/display" + "github.com/dfanso/commit-msg/src/internal/git" + "github.com/dfanso/commit-msg/src/internal/stats" "github.com/dfanso/commit-msg/src/types" "github.com/pterm/pterm" ) -// Normalize path to handle both forward and backslashes -func normalizePath(path string) string { - // Replace backslashes with forward slashes - normalized := strings.ReplaceAll(path, "\\", "/") - // Remove any trailing slash - normalized = strings.TrimSuffix(normalized, "/") - return normalized -} - -// Main function +// main is the entry point of the commit message generator func main() { // Get API key from environment variables var apiKey string @@ -55,7 +45,7 @@ func main() { } // Check if current directory is a git repository - if !isGitRepository(currentDir) { + if !git.IsRepository(currentDir) { log.Fatalf("Current directory is not a Git repository: %s", currentDir) } @@ -70,7 +60,7 @@ func main() { } // Get file statistics before fetching changes - fileStats, err := getFileStatistics(&repoConfig) + fileStats, err := stats.GetFileStatistics(&repoConfig) if err != nil { log.Fatalf("Failed to get file statistics: %v", err) } @@ -84,7 +74,7 @@ func main() { pterm.Println() // Display file statistics with icons - displayFileStatistics(fileStats) + display.ShowFileStatistics(fileStats) if fileStats.TotalFiles == 0 { pterm.Warning.Println("No changes detected in the Git repository.") @@ -92,7 +82,7 @@ func main() { } // Get the changes - changes, err := getGitChanges(&repoConfig) + changes, err := git.GetChanges(&repoConfig) if err != nil { log.Fatalf("Failed to get Git changes: %v", err) } @@ -131,7 +121,7 @@ func main() { pterm.Println() // Display the commit message in a styled panel - displayCommitMessage(commitMsg) + display.ShowCommitMessage(commitMsg) // Copy to clipboard err = clipboard.WriteAll(commitMsg) @@ -144,337 +134,5 @@ func main() { pterm.Println() // Display changes preview - displayChangesPreview(fileStats) -} - -// Check if directory is a git repository -func isGitRepository(path string) bool { - cmd := exec.Command("git", "-C", path, "rev-parse", "--is-inside-work-tree") - output, err := cmd.CombinedOutput() - if err != nil { - return false - } - return strings.TrimSpace(string(output)) == "true" -} - -// Get changes using Git -func getGitChanges(config *types.RepoConfig) (string, error) { - var changes strings.Builder - - // 1. Check for unstaged changes - cmd := exec.Command("git", "-C", config.Path, "diff", "--name-status") - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("git diff failed: %v", err) - } - - if len(output) > 0 { - changes.WriteString("Unstaged changes:\n") - changes.WriteString(string(output)) - changes.WriteString("\n\n") - - // Get the content of these changes - diffCmd := exec.Command("git", "-C", config.Path, "diff") - diffOutput, err := diffCmd.Output() - if err != nil { - return "", fmt.Errorf("git diff content failed: %v", err) - } - - changes.WriteString("Unstaged diff content:\n") - changes.WriteString(string(diffOutput)) - changes.WriteString("\n\n") - } - - // 2. Check for staged changes - stagedCmd := exec.Command("git", "-C", config.Path, "diff", "--name-status", "--cached") - stagedOutput, err := stagedCmd.Output() - if err != nil { - return "", fmt.Errorf("git diff --cached failed: %v", err) - } - - if len(stagedOutput) > 0 { - changes.WriteString("Staged changes:\n") - changes.WriteString(string(stagedOutput)) - changes.WriteString("\n\n") - - // Get the content of these changes - stagedDiffCmd := exec.Command("git", "-C", config.Path, "diff", "--cached") - stagedDiffOutput, err := stagedDiffCmd.Output() - if err != nil { - return "", fmt.Errorf("git diff --cached content failed: %v", err) - } - - changes.WriteString("Staged diff content:\n") - changes.WriteString(string(stagedDiffOutput)) - changes.WriteString("\n\n") - } - - // 3. Check for untracked files - untrackedCmd := exec.Command("git", "-C", config.Path, "ls-files", "--others", "--exclude-standard") - untrackedOutput, err := untrackedCmd.Output() - if err != nil { - return "", fmt.Errorf("git ls-files failed: %v", err) - } - - if len(untrackedOutput) > 0 { - changes.WriteString("Untracked files:\n") - changes.WriteString(string(untrackedOutput)) - changes.WriteString("\n\n") - - // Try to get content of untracked files (limited to text files and smaller size) - untrackedFiles := strings.Split(strings.TrimSpace(string(untrackedOutput)), "\n") - for _, file := range untrackedFiles { - if file == "" { - continue - } - - fullPath := filepath.Join(config.Path, file) - if isTextFile(fullPath) && isSmallFile(fullPath) { - fileContent, err := os.ReadFile(fullPath) - if err == nil { - changes.WriteString(fmt.Sprintf("Content of new file %s:\n", file)) - changes.WriteString(string(fileContent)) - changes.WriteString("\n\n") - } - } - } - } - - // 4. Get recent commits for context - recentCommitsCmd := exec.Command("git", "-C", config.Path, "log", "--oneline", "-n", "3") - recentCommitsOutput, err := recentCommitsCmd.Output() - if err == nil && len(recentCommitsOutput) > 0 { - changes.WriteString("Recent commits for context:\n") - changes.WriteString(string(recentCommitsOutput)) - changes.WriteString("\n") - } - - return changes.String(), nil -} - -// Check if a file is likely to be a text file -func isTextFile(filename string) bool { - // List of common text file extensions - textExtensions := []string{".txt", ".md", ".go", ".js", ".py", ".java", ".c", ".cpp", ".h", ".html", ".css", ".json", ".xml", ".yaml", ".yml", ".sh", ".bash", ".ts", ".tsx", ".jsx", ".php", ".rb", ".rs", ".dart"} - - ext := strings.ToLower(filepath.Ext(filename)) - for _, textExt := range textExtensions { - if ext == textExt { - return true - } - } - - return false -} - -// Check if a file is small enough to include in context -func isSmallFile(filename string) bool { - const maxSize = 10 * 1024 // 10KB max - - info, err := os.Stat(filename) - if err != nil { - return false - } - - 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") - } + display.ShowChangesPreview(fileStats) }