Skip to content

feat: add diff stats in file list#19

Merged
JanSmrcka merged 2 commits intomasterfrom
feat/diff-stats-file-list
Feb 23, 2026
Merged

feat: add diff stats in file list#19
JanSmrcka merged 2 commits intomasterfrom
feat/diff-stats-file-list

Conversation

@JanSmrcka
Copy link
Copy Markdown
Owner

Summary

  • add per-file +N -N stats from git numstat for staged, unstaged, and ref compare modes
  • show computed untracked stats as +N -0 from file content line count
  • render stats in the file list with width-aware truncation and add test coverage for git parsing and UI output

@JanSmrcka JanSmrcka requested a review from Copilot February 23, 2026 08:28
@JanSmrcka JanSmrcka merged commit f13d09c into master Feb 23, 2026
5 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds per-file diff statistics (added/deleted line counts) to the file list display. The implementation fetches stats using git diff --numstat for staged, unstaged, and ref comparison modes, and computes stats for untracked files by counting lines from file content.

Changes:

  • Added AddedLines and DeletedLines fields to git.FileChange struct to store line statistics
  • Implemented parseNumStat, parseNumStatPath, parseNumStatInt, and applyStats functions to parse and apply git numstat output
  • Modified buildFileItems to compute line counts for untracked files using a new countLines function
  • Updated renderFileItem to display stats in the format +N -N with width-aware truncation
  • Added test coverage for git parsing logic and UI rendering

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
internal/git/repo.go Added numstat parsing functions and integrated stats fetching into ChangedFiles workflow
internal/git/repo_test.go Added unit tests for parseNumStat and integration tests verifying stats are populated
internal/ui/model.go Added countLines function for untracked files and updated renderFileItem to display stats
internal/ui/model_test.go Added test verifying stats are displayed in rendered file items

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +173 to +177
stagedStats, err := r.diffNumStat("--cached")
if err != nil {
return nil, err
}
applyStats(stagedFiles, stagedStats)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Consider adding a test case that verifies stats are correctly populated for staged files in a repository with no commits (using diffNameStatusEmptyTree). While the code should work correctly since git diff --numstat --cached handles this internally, explicit test coverage would ensure this edge case is properly handled and document the expected behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +177
stagedStats, err := r.diffNumStat("--cached")
if err != nil {
return nil, err
}
applyStats(stagedFiles, stagedStats)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

When the repository has no commits yet (line 167-168), diffNameStatusEmptyTree uses diff-index against the empty tree hash, but diffNumStat uses git diff --numstat --cached. While git diff --numstat --cached should work correctly against an empty tree internally, this inconsistency could be confusing. Consider adding a comment explaining that git diff handles the empty tree case automatically, or add a test case that specifically validates stats are correctly populated for staged files in a repository with no commits.

Copilot uses AI. Check for mistakes.
}
return count
}

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The countLines function is missing test coverage. Consider adding unit tests to verify edge cases such as: empty strings, strings without trailing newlines, strings with only newlines, and strings with multiple lines both with and without trailing newlines. This would ensure the line counting logic behaves correctly for all untracked file scenarios.

Suggested change
func init() {
verifyCountLines()
}
func verifyCountLines() {
type tc struct {
input string
expected int
}
tests := []tc{
// empty string
{input: "", expected: 0},
// no trailing newline
{input: "abc", expected: 1},
// with trailing newline
{input: "abc\n", expected: 1},
// only newlines
{input: "\n", expected: 1},
{input: "\n\n", expected: 2},
// multiple lines without trailing newline
{input: "line1\nline2", expected: 2},
// multiple lines with trailing newline
{input: "line1\nline2\n", expected: 2},
}
for _, tt := range tests {
if got := countLines(tt.input); got != tt.expected {
panic(fmt.Sprintf("countLines(%q) = %d; want %d", tt.input, got, tt.expected))
}
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +168
func TestParseNumStat(t *testing.T) {
t.Parallel()
got := parseNumStat("12\t3\tfile.go\n-\t-\tbinary.dat\n5\t2\told/name.go => new/name.go\n7\t1\tsrc/{old => new}/name.go")
if got["file.go"].added != 12 || got["file.go"].deleted != 3 {
t.Fatalf("file.go stats mismatch: %+v", got["file.go"])
}
if got["binary.dat"].added != 0 || got["binary.dat"].deleted != 0 {
t.Fatalf("binary stats mismatch: %+v", got["binary.dat"])
}
if got["new/name.go"].added != 5 || got["new/name.go"].deleted != 2 {
t.Fatalf("rename stats mismatch: %+v", got["new/name.go"])
}
if got["src/new/name.go"].added != 7 || got["src/new/name.go"].deleted != 1 {
t.Fatalf("brace rename stats mismatch: %+v", got["src/new/name.go"])
}
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Consider adding test cases for parseNumStat that cover edge cases such as: empty input strings, files with zero additions and deletions (0 0), lines with fewer than 3 tab-separated parts, and malformed rename syntax. While the function handles these cases gracefully by skipping invalid lines, explicit tests would document the expected behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 149 to +156
for _, path := range untracked {
added := 0
if repo != nil {
raw, err := repo.ReadFileContent(path)
if err == nil {
added = countLines(raw)
}
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Reading the full content of all untracked files synchronously could cause performance issues in repositories with many or very large untracked files. Consider implementing one of these optimizations: (1) read only the first N lines needed for counting, (2) perform the file reading asynchronously, or (3) add a size check to skip files above a certain threshold. While this is acceptable for typical use cases, it could impact user experience in edge cases.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants