Skip to content

Conversation

@DFanso
Copy link
Owner

@DFanso DFanso commented Oct 3, 2025

Description

This PR significantly enhances the commit message generator with a comprehensive UI overhaul, automatic clipboard functionality, and major code refactoring. The changes provide users with clear, colorful feedback about their Git repository state, automatically copy generated messages to clipboard, and establish a clean, maintainable codebase through modular architecture.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement- [ ] Other (please describe):

Changes Made

Code Improvements & Refactoring

Architecture Improvements

  • Modular Structure: Refactored monolithic main.go (481 lines) into organized modules
  • Internal Packages: Created internal/ directory following Go best practices
  • Separation of Concerns: Each module has a single, well-defined responsibility
  • Improved Maintainability: Easier to locate, modify, and test specific functionality

Data Structures

  • FileStatistics: Comprehensive struct to track:
    • Staged files list
    • Unstaged files list
    • Untracked files list
    • Total file count
    • Lines added/deleted statistics

Refactored Functions (with proper encapsulation)

  • Display Package (internal/display):

    • ShowFileStatistics(): Renders file stats with pterm styling
    • ShowCommitMessage(): Displays commit message in styled panel
    • ShowChangesPreview(): Shows line change statistics table
  • Git Package (internal/git):

    • IsRepository(): Validates Git repository
    • GetChanges(): Retrieves all Git changes (staged, unstaged, untracked)
  • Stats Package (internal/stats):

    • GetFileStatistics(): Collects comprehensive file statistics from Git
  • Utils Package (internal/utils):

    • NormalizePath(): Cross-platform path normalization
    • IsTextFile(): Text file detection by extension
    • IsSmallFile(): Size-based file filtering
    • FilterEmpty(): String slice cleanup utility

Benefits of Refactoring

  • Maintainability: Easier to locate and modify specific functionality
  • Testability: Each package can be tested independently
  • Readability: Clear module boundaries and responsibilities
  • Scalability: Simple to add new features without affecting existing code
  • Collaboration: Team members can work on different packages simultaneously
  • Go Idioms: Follows Go best practices with internal packages
  • Reduced Complexity: Main file reduced from 481 to 139 lines (71% reduction)

Dependency Updates

  • Added github.com/atotto/clipboard v0.1.4 for clipboard functionality
  • Updated go.mod and go.sum with new dependencies and reordered entries

Commits Included

This PR includes the following commits:
refactor: reorganize code into modular package structure

  • Refactor 481-line main.go into organized modules
  • Create internal/ package structure following Go best practices
  • Separate concerns: display, git operations, statistics, utilities
  • Export public functions with proper naming conventions
  • Improve code maintainability and testability
  • No functional changes - pure refactoring for better architecture

Testing

  • Tested with Gemini API
  • Tested with Grok API
  • Tested on Windows
  • Tested on Linux
  • Tested on macOS
  • Added/updated tests (if applicable)

Test Scenarios Verified

  1. ✅ Repository with staged files only
  2. ✅ Repository with unstaged files only
  3. ✅ Repository with untracked files only
  4. ✅ Repository with mixed file states
  5. ✅ Repository with no changes (displays warning)
  6. ✅ Large number of files (collapse functionality works)
  7. ✅ Line statistics calculation for staged changes
  8. ✅ Clipboard copy functionality (Windows)
  9. ✅ Error handling when clipboard is unavailable
  10. ✅ Refactored code compiles successfully
  11. ✅ All modules integrate properly
  12. ✅ No linter errors or warnings

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have tested this in a real Git repository
  • I have read the CONTRIBUTING.md guidelines

Additional Notes

Technical Details

  • Architecture: Modular package-based structure with internal/ packages
  • Performance: File statistics collected before fetching full Git changes
  • Optimization: Early exit when no changes detected
  • Accuracy: Statistics calculation processes staged changes only
  • Filtering: Binary and large files excluded from detailed analysis
  • Code Organization:
    • Main entry point: 139 lines (down from 481 - 71% reduction)
    • Display module: 134 lines
    • Git operations: 118 lines
    • Statistics: 75 lines
    • Utilities: 59 lines
    • Total: 525 lines (well-organized vs 481 monolithic lines)

Breaking Changes

None. This is a pure enhancement that maintains full backward compatibility. No API changes or functionality modifications.


For Hacktoberfest Participants

  • This PR is submitted as part of Hacktoberfest 2025

Thank you for your contribution! 🎉

Summary by CodeRabbit

  • New Features

    • Visual summaries for staged, unstaged, and untracked files with counts and sample entries.
    • Styled commit message panel and changes preview (lines added/deleted, total files).
    • Aggregated change details including recent commits and automated Git repository detection.
    • Progress spinners and environment-based API key validation.
  • Refactor

    • Core workflow reorganized into reusable internal modules for clearer output and smoother operation.
  • Documentation

    • README updated with a Pull Request Reviews badge.

- Moved Git operations, stats, and display functions to
- src/internal/git, src/internal/stats, and src/internal/display.
- Added src/internal/utils for utilities. Updated README.md with
- CodeRabbit badge to enhance documentation.
@DFanso DFanso self-assigned this Oct 3, 2025
@DFanso DFanso added the hacktoberfest Eligible for Hacktoberfest label Oct 3, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

Walkthrough

Adds internal packages for display, git operations, stats, and utilities; refactors main to use these APIs and environment-based API key handling; introduces spinner-based progress and a README badge update. New exported types/functions provide file statistics gathering, git change aggregation, and styled terminal rendering.

Changes

Cohort / File(s) Summary of Changes
Documentation
README.md
Replaced a README badge line; documentation-only update.
Display subsystem
src/internal/display/.../display.go
New FileStatistics type and display functions: ShowFileStatistics, ShowCommitMessage, ShowChangesPreview using pterm to render lists, panels, and small tables.
Git operations
src/internal/git/.../operations.go
New git utilities: IsRepository(path string) bool and GetChanges(config *types.RepoConfig) (string, error) to detect repo state and assemble staged/unstaged/untracked changes (with selective file content and recent commits).
Statistics gathering
src/internal/stats/.../statistics.go
New GetFileStatistics(config *types.RepoConfig) (*display.FileStatistics, error) collecting staged/unstaged/untracked files and parsing git diff --cached --numstat to compute lines added/deleted and total files.
Utilities
src/internal/utils/.../utils.go
New helpers: NormalizePath, IsTextFile, IsSmallFile, FilterEmpty for path normalization, file-type/size checks, and slice filtering.
Main integration
src/main.go
Refactored to call new package APIs (git.IsRepository, stats.GetFileStatistics, git.GetChanges, and display functions); added environment-based API key handling and spinner/progress display.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Main as main.go
  participant Git as internal/git
  participant Stats as internal/stats
  participant Disp as internal/display
  participant Utils as internal/utils

  User->>Main: run CLI
  Main->>Git: IsRepository(path)
  Git-->>Main: bool
  alt not a repo
    Main-->>User: exit with message
  else repo
    Main->>Stats: GetFileStatistics(config)
    Stats->>Git: run git diff/ls-files/numstat
    Stats->>Utils: normalize/filter results
    Stats-->>Main: FileStatistics
    Main->>Disp: ShowFileStatistics(stats)
    Main->>Git: GetChanges(config)
    Git->>Utils: check files (IsTextFile/IsSmallFile)
    Git-->>Main: aggregated changes string
    Main->>Disp: ShowCommitMessage(message)
    Main->>Disp: ShowChangesPreview(stats)
    Main-->>User: rendered output
  end
  note right of Main: spinner shown during operations
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble diffs beneath moonlight hush,
Carrots of lines and files in a rush.
I color the burrow with tidy lists bright,
Hop, render, and hum through each git-night.
A rabbit-approved patch — snug, neat, and light. 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit's high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely summarizes the primary structural change by indicating that internal modules were extracted for better organization, which matches the pull request’s main refactoring objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39c355b and 7615191.

📒 Files selected for processing (1)
  • src/internal/git/operations.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/internal/git/operations.go (2)
src/types/types.go (1)
  • RepoConfig (10-13)
src/internal/utils/utils.go (2)
  • IsTextFile (19-35)
  • IsSmallFile (38-47)
🔇 Additional comments (3)
src/internal/git/operations.go (3)

15-22: LGTM!

The implementation correctly checks for a Git repository using git rev-parse --is-inside-work-tree. The use of CombinedOutput() and silent error handling is appropriate here since returning false for any failure (including non-git directories) is the expected behavior.


88-106: Past issue resolved with explicit error handling.

The inconsistent error handling for untracked file reads flagged in the previous review has been addressed. The code now explicitly handles os.ReadFile errors with a continue statement and includes a comment explaining why it's safe to ignore (lines 98-101).

The implementation correctly prevents one inaccessible file from blocking the entire change collection process.


110-116: LGTM!

The silent error handling for recent commits is appropriate since this information is optional context. The function continues gracefully if commit history is unavailable, which is the correct behavior for a non-critical operation.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main.go (2)

114-117: Simplify error handling on commit message generation failure.

The spinner failure message may not be visible before log.Fatalf terminates the program. Consider removing the spinner failure call or using pterm.Error instead of log.Fatalf to ensure the spinner message is displayed.

Apply this diff:

 	if err != nil {
-		spinnerGenerating.Fail("Failed to generate commit message")
-		log.Fatalf("Error: %v", err)
+		spinnerGenerating.Fail(fmt.Sprintf("Failed to generate commit message: %v", err))
+		os.Exit(1)
 	}

22-23: Replace “google” with “gemini” in COMMIT_LLM checks
Update src/main.go (lines 22 and 106) to check for COMMIT_LLM == "gemini" and read GEMINI_API_KEY (not "google"/GOOGLE_API_KEY) so it aligns with README.

🧹 Nitpick comments (5)
src/internal/utils/utils.go (3)

10-16: Consider using filepath.ToSlash for idiomatic path normalization.

The current implementation manually replaces backslashes, but Go's filepath.ToSlash() provides the same functionality in a more idiomatic way.

Apply this diff to use the standard library function:

-// NormalizePath handles both forward and backslashes
 func NormalizePath(path string) string {
-	// Replace backslashes with forward slashes
-	normalized := strings.ReplaceAll(path, "\\", "/")
+	// Use filepath.ToSlash for cross-platform path normalization
+	normalized := filepath.ToSlash(path)
 	// Remove any trailing slash
 	normalized = strings.TrimSuffix(normalized, "/")
 	return normalized
 }

19-35: Consider expanding the text file extension list.

The current list covers common cases, but consider adding extensions like .sql, .swift, .kt, .scala, .r, .groovy for broader language support.

This is a low-priority enhancement that can be deferred.


50-58: Consider filtering whitespace-only strings.

The function currently only filters empty strings (""), but git output might contain whitespace-only entries. Consider using strings.TrimSpace(s) != "" for more robust filtering.

Apply this diff if whitespace-only strings should be filtered:

 func FilterEmpty(slice []string) []string {
 	filtered := []string{}
 	for _, s := range slice {
-		if s != "" {
+		if strings.TrimSpace(s) != "" {
 			filtered = append(filtered, s)
 		}
 	}
 	return filtered
 }
src/internal/git/operations.go (1)

25-117: Consider limiting the size of the returned change summary.

For repositories with large changesets, the returned string could become very large and cause memory issues. Consider implementing a size limit or truncation strategy.

Example approach:

const maxChangeSize = 1024 * 1024 // 1MB limit

func GetChanges(config *types.RepoConfig) (string, error) {
	var changes strings.Builder
	// ... existing logic ...
	
	result := changes.String()
	if len(result) > maxChangeSize {
		return result[:maxChangeSize] + "\n\n... (truncated due to size)", nil
	}
	return result, nil
}
src/internal/display/display.go (1)

26-93: Consider extracting the repetitive bullet list logic.

The code follows the same pattern for staged, unstaged, and untracked files (create header item, add files up to limit, add "more" indicator). This repetition could be reduced with a helper function.

Example refactor:

func addFileSection(bulletItems *[]pterm.BulletListItem, files []string, label string, color pterm.Color, icon string, limit int) {
	if len(files) == 0 {
		return
	}
	
	style := pterm.NewStyle(color)
	*bulletItems = append(*bulletItems, pterm.BulletListItem{
		Level:       0,
		Text:        style.Sprint(fmt.Sprintf("%s %s: %d", icon, label, len(files))),
		TextStyle:   style,
		BulletStyle: style,
	})
	
	for i, file := range files {
		if i < limit {
			*bulletItems = append(*bulletItems, pterm.BulletListItem{
				Level: 1,
				Text:  file,
			})
		}
	}
	
	if len(files) > limit {
		*bulletItems = append(*bulletItems, pterm.BulletListItem{
			Level: 1,
			Text:  pterm.Gray(fmt.Sprintf("... and %d more", len(files)-limit)),
		})
	}
}

func ShowFileStatistics(stats *FileStatistics) {
	pterm.DefaultSection.Println("📊 Changes Summary")
	bulletItems := []pterm.BulletListItem{}
	
	addFileSection(&bulletItems, stats.StagedFiles, "Staged files", pterm.FgGreen, "✅", 5)
	addFileSection(&bulletItems, stats.UnstagedFiles, "Unstaged files", pterm.FgYellow, "⚠️", 3)
	addFileSection(&bulletItems, stats.UntrackedFiles, "Untracked files", pterm.FgCyan, "📝", 3)
	
	pterm.DefaultBulletList.WithItems(bulletItems).Render()
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02b96a1 and 39c355b.

📒 Files selected for processing (6)
  • README.md (1 hunks)
  • src/internal/display/display.go (1 hunks)
  • src/internal/git/operations.go (1 hunks)
  • src/internal/stats/statistics.go (1 hunks)
  • src/internal/utils/utils.go (1 hunks)
  • src/main.go (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/internal/stats/statistics.go (3)
src/types/types.go (1)
  • RepoConfig (10-13)
src/internal/display/display.go (1)
  • FileStatistics (10-17)
src/internal/utils/utils.go (1)
  • FilterEmpty (50-58)
src/internal/git/operations.go (2)
src/types/types.go (1)
  • RepoConfig (10-13)
src/internal/utils/utils.go (2)
  • IsTextFile (19-35)
  • IsSmallFile (38-47)
src/main.go (3)
src/internal/git/operations.go (2)
  • IsRepository (15-22)
  • GetChanges (25-117)
src/internal/stats/statistics.go (1)
  • GetFileStatistics (14-74)
src/internal/display/display.go (3)
  • ShowFileStatistics (20-96)
  • ShowCommitMessage (99-115)
  • ShowChangesPreview (118-133)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build Go Binary (windows-latest)
🔇 Additional comments (4)
src/internal/git/operations.go (1)

15-22: LGTM!

The implementation correctly checks if a directory is a git repository using the standard git command. Error handling is appropriate for a boolean check function.

src/internal/display/display.go (2)

99-115: LGTM!

The commit message display implementation follows pterm best practices with appropriate styling and clear visual hierarchy.

Based on learnings.


118-133: LGTM!

The changes preview implementation appropriately handles both cases (with and without line statistics) and provides clear visual feedback.

src/internal/stats/statistics.go (1)

56-68: Numstat parsing with strings.Fields is safe
strings.Fields correctly splits on tabs to yield the added/deleted counts; filenames with spaces aren’t used here.

Comment on lines +22 to +40
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")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate git command errors to the caller.

The function silently ignores all errors from git commands. If git is not installed, the repository is corrupted, or there's a permission issue, the function returns empty statistics without any indication of failure. This makes debugging difficult and could mask critical issues.

Consider propagating the first encountered error:

 	// Get staged files
 	stagedCmd := exec.Command("git", "-C", config.Path, "diff", "--name-only", "--cached")
 	stagedOutput, err := stagedCmd.Output()
-	if err == nil && len(stagedOutput) > 0 {
+	if err != nil {
+		return nil, fmt.Errorf("failed to get staged files: %w", err)
+	}
+	if 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 {
+	if err != nil {
+		return nil, fmt.Errorf("failed to get unstaged files: %w", err)
+	}
+	if 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 {
+	if err != nil {
+		return nil, fmt.Errorf("failed to get untracked files: %w", err)
+	}
+	if len(untragedOutput) > 0 {
 		stats.UntrackedFiles = strings.Split(strings.TrimSpace(string(untrackedOutput)), "\n")
 	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/internal/stats/statistics.go around lines 22 to 40, the code currently
ignores errors from the git exec.Command.Output() calls; update the function to
propagate the first encountered error back to the caller instead of silently
continuing: after each Output() call check if err != nil and immediately return
the error (or wrap it with context), otherwise process the output as currently
done (trim/split and assign to stats fields only when non-empty); ensure the
function signature is updated to return an error if it doesn't already and
include contextual messages (e.g., which git subcommand failed) when returning
the error.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@DFanso DFanso added the hacktoberfest-accepted Approved Hacktoberfest contribution label Oct 3, 2025
@DFanso DFanso merged commit 5db93cf into main Oct 3, 2025
8 checks passed
DFanso pushed a commit that referenced this pull request Oct 12, 2025
Fix #113 : add token count, cost estimation, and processing time to dry run mode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hacktoberfest Eligible for Hacktoberfest hacktoberfest-accepted Approved Hacktoberfest contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants