From 32dcf273ee4eb03e4fa2ffc12172bb34352645bc Mon Sep 17 00:00:00 2001 From: Yuya Fujiwara Date: Wed, 28 Jan 2026 19:07:11 +0900 Subject: [PATCH 1/7] feat(git): add lock file detection utility --- internal/git/lockfile.go | 41 ++++++++++++++++++++++++++++++++ internal/git/lockfile_test.go | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 internal/git/lockfile.go create mode 100644 internal/git/lockfile_test.go diff --git a/internal/git/lockfile.go b/internal/git/lockfile.go new file mode 100644 index 0000000..82b8ed8 --- /dev/null +++ b/internal/git/lockfile.go @@ -0,0 +1,41 @@ +package git + +import ( + "path/filepath" + "strings" +) + +var lockFileNames = map[string]bool{ + "uv.lock": true, + "poetry.lock": true, + "package-lock.json": true, + "yarn.lock": true, + "pnpm-lock.yaml": true, + "Gemfile.lock": true, + "Cargo.lock": true, + "go.sum": true, + "composer.lock": true, +} + +var lockFileSuffixes = []string{ + ".lock", + "-lock.json", + "-lock.yaml", +} + +func IsLockFile(filename string) bool { + base := filepath.Base(filename) + + if lockFileNames[base] { + return true + } + + lower := strings.ToLower(base) + for _, suffix := range lockFileSuffixes { + if strings.HasSuffix(lower, suffix) { + return true + } + } + + return false +} diff --git a/internal/git/lockfile_test.go b/internal/git/lockfile_test.go new file mode 100644 index 0000000..df1f763 --- /dev/null +++ b/internal/git/lockfile_test.go @@ -0,0 +1,44 @@ +package git + +import "testing" + +func TestIsLockFile(t *testing.T) { + tests := []struct { + name string + filename string + want bool + }{ + // Lock files that should be detected + {"uv.lock", "uv.lock", true}, + {"poetry.lock", "poetry.lock", true}, + {"package-lock.json", "package-lock.json", true}, + {"yarn.lock", "yarn.lock", true}, + {"pnpm-lock.yaml", "pnpm-lock.yaml", true}, + {"Gemfile.lock", "Gemfile.lock", true}, + {"Cargo.lock", "Cargo.lock", true}, + {"go.sum", "go.sum", true}, + {"composer.lock", "composer.lock", true}, + + // Lock files in subdirectories + {"nested uv.lock", "packages/uv.lock", true}, + {"nested package-lock.json", "frontend/package-lock.json", true}, + + // Non-lock files + {"regular go file", "main.go", false}, + {"regular json", "config.json", false}, + {"go.mod", "go.mod", false}, + {"package.json", "package.json", false}, + {"Gemfile", "Gemfile", false}, + {"Cargo.toml", "Cargo.toml", false}, + {"pyproject.toml", "pyproject.toml", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsLockFile(tt.filename) + if got != tt.want { + t.Errorf("IsLockFile(%q) = %v, want %v", tt.filename, got, tt.want) + } + }) + } +} From 3cc804867491f6fa363001e26f52aca1a7c7d711 Mon Sep 17 00:00:00 2001 From: Yuya Fujiwara Date: Wed, 28 Jan 2026 19:08:50 +0900 Subject: [PATCH 2/7] feat(lockfile): add staged diff summary with lock file handling Add ParseNumstat to parse git numstat output and StagedDiffWithSummary to generate diffs that include full content for regular files but only line count summaries for lock files. This reduces noise in commit diffs when lock files have many changes. --- internal/git/lockfile.go | 95 ++++++++++++++++++++++++++++++ internal/git/lockfile_test.go | 105 +++++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/internal/git/lockfile.go b/internal/git/lockfile.go index 82b8ed8..fe50084 100644 --- a/internal/git/lockfile.go +++ b/internal/git/lockfile.go @@ -1,7 +1,11 @@ package git import ( + "bytes" + "fmt" + "os/exec" "path/filepath" + "strconv" "strings" ) @@ -39,3 +43,94 @@ func IsLockFile(filename string) bool { return false } + +type FileStat struct { + Filename string + Added int + Deleted int + Binary bool +} + +func ParseNumstat(output string) []FileStat { + var stats []FileStat + lines := strings.Split(output, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + parts := strings.Split(line, "\t") + if len(parts) != 3 { + continue + } + + stat := FileStat{Filename: parts[2]} + + if parts[0] == "-" && parts[1] == "-" { + stat.Binary = true + } else { + added, _ := strconv.Atoi(parts[0]) + deleted, _ := strconv.Atoi(parts[1]) + stat.Added = added + stat.Deleted = deleted + } + + stats = append(stats, stat) + } + + return stats +} + +func StagedDiffWithSummary() (string, error) { + numstatCmd := exec.Command("git", "diff", "--staged", "--numstat") + var numstatOut bytes.Buffer + var numstatErr bytes.Buffer + numstatCmd.Stdout = &numstatOut + numstatCmd.Stderr = &numstatErr + if err := numstatCmd.Run(); err != nil { + return "", fmt.Errorf("git diff --numstat failed: %v: %s", err, strings.TrimSpace(numstatErr.String())) + } + + stats := ParseNumstat(numstatOut.String()) + lockFiles := make(map[string]FileStat) + for _, stat := range stats { + if IsLockFile(stat.Filename) { + lockFiles[stat.Filename] = stat + } + } + + if len(lockFiles) == 0 { + return StagedDiff() + } + + var result strings.Builder + + for _, stat := range stats { + if _, isLock := lockFiles[stat.Filename]; isLock { + result.WriteString(fmt.Sprintf("diff --git a/%s b/%s\n", stat.Filename, stat.Filename)) + result.WriteString(fmt.Sprintf("[Lock file: +%d -%d lines, content omitted]\n\n", stat.Added, stat.Deleted)) + } else { + fileDiff, err := stagedDiffForFile(stat.Filename) + if err != nil { + return "", err + } + result.WriteString(fileDiff) + } + } + + return result.String(), nil +} + +func stagedDiffForFile(filename string) (string, error) { + cmd := exec.Command("git", "diff", "--staged", "--", filename) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("git diff for %s failed: %v: %s", filename, err, strings.TrimSpace(stderr.String())) + } + return stdout.String(), nil +} diff --git a/internal/git/lockfile_test.go b/internal/git/lockfile_test.go index df1f763..48c7d0f 100644 --- a/internal/git/lockfile_test.go +++ b/internal/git/lockfile_test.go @@ -1,6 +1,10 @@ package git -import "testing" +import ( + "strconv" + "strings" + "testing" +) func TestIsLockFile(t *testing.T) { tests := []struct { @@ -42,3 +46,102 @@ func TestIsLockFile(t *testing.T) { }) } } + +func TestParseNumstat(t *testing.T) { + // git diff --numstat output format: addeddeletedfilename + input := strings.Join([]string{ + "10\t5\tmain.go", + "500\t200\tuv.lock", + "3\t1\tREADME.md", + }, "\n") + + stats := ParseNumstat(input) + + if len(stats) != 3 { + t.Fatalf("expected 3 stats, got %d", len(stats)) + } + + tests := []struct { + filename string + added int + deleted int + }{ + {"main.go", 10, 5}, + {"uv.lock", 500, 200}, + {"README.md", 3, 1}, + } + + for i, tt := range tests { + if stats[i].Filename != tt.filename { + t.Errorf("stats[%d].Filename = %q, want %q", i, stats[i].Filename, tt.filename) + } + if stats[i].Added != tt.added { + t.Errorf("stats[%d].Added = %d, want %d", i, stats[i].Added, tt.added) + } + if stats[i].Deleted != tt.deleted { + t.Errorf("stats[%d].Deleted = %d, want %d", i, stats[i].Deleted, tt.deleted) + } + } +} + +func TestParseNumstatBinaryFile(t *testing.T) { + // Binary files show as "-" in numstat + input := "-\t-\timage.png" + + stats := ParseNumstat(input) + + if len(stats) != 1 { + t.Fatalf("expected 1 stat, got %d", len(stats)) + } + + if stats[0].Filename != "image.png" { + t.Errorf("Filename = %q, want %q", stats[0].Filename, "image.png") + } + if !stats[0].Binary { + t.Error("expected Binary to be true") + } +} + +func TestStagedDiffWithSummary(t *testing.T) { + repo := setupRepo(t) + withRepo(t, repo, func() { + // Create a regular file and a lock file + writeFile(t, repo, "main.go", "package main\n\nfunc main() {}\n") + writeLargeLockFile(t, repo, "uv.lock", 100) + + runGit(t, repo, "add", "main.go", "uv.lock") + + diff, err := StagedDiffWithSummary() + if err != nil { + t.Fatalf("StagedDiffWithSummary error: %v", err) + } + + // Should contain full diff for main.go + if !strings.Contains(diff, "package main") { + t.Error("expected diff to contain main.go content") + } + + // Should contain summary for uv.lock, not full content + if !strings.Contains(diff, "uv.lock") { + t.Error("expected diff to mention uv.lock") + } + if strings.Contains(diff, "package-version-1") { + t.Error("expected diff NOT to contain uv.lock content details") + } + // Should contain line count summary + if !strings.Contains(diff, "100") { + t.Error("expected diff to contain line count for uv.lock") + } + }) +} + +func writeLargeLockFile(t *testing.T, repo, name string, lines int) { + t.Helper() + var content strings.Builder + for i := 0; i < lines; i++ { + content.WriteString("package-version-") + content.WriteString(strconv.Itoa(i)) + content.WriteString(" = \"1.0.0\"\n") + } + writeFile(t, repo, name, content.String()) +} From f20d370c7a5eaea0b6b7d4f0b55e7d2db5061b58 Mon Sep 17 00:00:00 2001 From: Yuya Fujiwara Date: Wed, 28 Jan 2026 19:10:35 +0900 Subject: [PATCH 3/7] refactor(commitDiff): use StagedDiffWithSummary instead of StagedDiff --- internal/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/app.go b/internal/app/app.go index 45f98d5..bade76b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -145,7 +145,7 @@ func commitDiff(amend bool) (string, error) { if amend { return git.LastCommitDiff() } - return git.StagedDiff() + return git.StagedDiffWithSummary() } func stageChanges(addAll bool, includeFiles []string) (func(), error) { From 5805da9aa3a602eed353a2039c09107c4cb5be82 Mon Sep 17 00:00:00 2001 From: Yuya Fujiwara Date: Wed, 28 Jan 2026 19:11:17 +0900 Subject: [PATCH 4/7] test(lockfile): add edge case tests for StagedDiffWithSummary Add tests for when only lock files are staged and when no lock files are present in the diff. These tests verify that lock file summaries are generated correctly and that regular files show their full content without lock file markers. --- internal/git/lockfile_test.go | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/internal/git/lockfile_test.go b/internal/git/lockfile_test.go index 48c7d0f..4a94b5b 100644 --- a/internal/git/lockfile_test.go +++ b/internal/git/lockfile_test.go @@ -145,3 +145,56 @@ func writeLargeLockFile(t *testing.T, repo, name string, lines int) { } writeFile(t, repo, name, content.String()) } + +func TestStagedDiffWithSummaryOnlyLockFile(t *testing.T) { + repo := setupRepo(t) + withRepo(t, repo, func() { + // Only stage a lock file + writeLargeLockFile(t, repo, "package-lock.json", 50) + runGit(t, repo, "add", "package-lock.json") + + diff, err := StagedDiffWithSummary() + if err != nil { + t.Fatalf("StagedDiffWithSummary error: %v", err) + } + + // Should contain summary + if !strings.Contains(diff, "package-lock.json") { + t.Error("expected diff to mention package-lock.json") + } + if !strings.Contains(diff, "Lock file") { + t.Error("expected diff to contain lock file summary marker") + } + // Should NOT contain actual content + if strings.Contains(diff, "package-version-") { + t.Error("expected diff NOT to contain lock file content") + } + }) +} + +func TestStagedDiffWithSummaryNoLockFiles(t *testing.T) { + repo := setupRepo(t) + withRepo(t, repo, func() { + // Only regular files + writeFile(t, repo, "main.go", "package main\n") + writeFile(t, repo, "util.go", "package util\n") + runGit(t, repo, "add", "main.go", "util.go") + + diff, err := StagedDiffWithSummary() + if err != nil { + t.Fatalf("StagedDiffWithSummary error: %v", err) + } + + // Should contain full diff + if !strings.Contains(diff, "package main") { + t.Error("expected diff to contain main.go content") + } + if !strings.Contains(diff, "package util") { + t.Error("expected diff to contain util.go content") + } + // Should NOT contain lock file marker + if strings.Contains(diff, "Lock file") { + t.Error("expected diff NOT to contain lock file marker") + } + }) +} From 09a24cb5e0a74164559735648c355df66c69d5c3 Mon Sep 17 00:00:00 2001 From: Yuya Fujiwara Date: Wed, 28 Jan 2026 19:51:13 +0900 Subject: [PATCH 5/7] refactor(lockfile): simplify lock file detection by removing redundant entries Removed lock files that match common suffixes (.lock, .yaml, -lock.json) to eliminate duplication. The suffix-based detection already covers these files, so maintaining them in the explicit map is unnecessary. --- internal/git/lockfile.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/internal/git/lockfile.go b/internal/git/lockfile.go index fe50084..a22dcde 100644 --- a/internal/git/lockfile.go +++ b/internal/git/lockfile.go @@ -9,16 +9,9 @@ import ( "strings" ) +// lockFileNames contains lock files that don't match common suffixes var lockFileNames = map[string]bool{ - "uv.lock": true, - "poetry.lock": true, - "package-lock.json": true, - "yarn.lock": true, - "pnpm-lock.yaml": true, - "Gemfile.lock": true, - "Cargo.lock": true, - "go.sum": true, - "composer.lock": true, + "go.sum": true, } var lockFileSuffixes = []string{ From 5aa566a31112303d318bae419368025221558e49 Mon Sep 17 00:00:00 2001 From: Yuya Fujiwara Date: Wed, 28 Jan 2026 20:06:38 +0900 Subject: [PATCH 6/7] feat(lockfile): add threshold-based lock file summarization Add a configurable threshold (LockFileSummaryThreshold = 200 lines) to only summarize lock files with significant changes. Lock files below the threshold are now shown in full diff, improving visibility for small updates while keeping large lock file diffs manageable. --- internal/git/lockfile.go | 20 +++++++++--- internal/git/lockfile_test.go | 58 +++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/internal/git/lockfile.go b/internal/git/lockfile.go index a22dcde..65e2006 100644 --- a/internal/git/lockfile.go +++ b/internal/git/lockfile.go @@ -20,6 +20,10 @@ var lockFileSuffixes = []string{ "-lock.yaml", } +// LockFileSummaryThreshold is the minimum number of changed lines +// before a lock file diff is summarized instead of shown in full. +const LockFileSummaryThreshold = 200 + func IsLockFile(filename string) bool { base := filepath.Base(filename) @@ -87,23 +91,29 @@ func StagedDiffWithSummary() (string, error) { } stats := ParseNumstat(numstatOut.String()) - lockFiles := make(map[string]FileStat) + + // Identify large lock files that should be summarized + largeLockFiles := make(map[string]FileStat) for _, stat := range stats { if IsLockFile(stat.Filename) { - lockFiles[stat.Filename] = stat + totalChanges := stat.Added + stat.Deleted + if totalChanges >= LockFileSummaryThreshold { + largeLockFiles[stat.Filename] = stat + } } } - if len(lockFiles) == 0 { + // If no large lock files, return standard diff + if len(largeLockFiles) == 0 { return StagedDiff() } var result strings.Builder for _, stat := range stats { - if _, isLock := lockFiles[stat.Filename]; isLock { + if lockStat, isLargeLock := largeLockFiles[stat.Filename]; isLargeLock { result.WriteString(fmt.Sprintf("diff --git a/%s b/%s\n", stat.Filename, stat.Filename)) - result.WriteString(fmt.Sprintf("[Lock file: +%d -%d lines, content omitted]\n\n", stat.Added, stat.Deleted)) + result.WriteString(fmt.Sprintf("[Lock file: +%d -%d lines, content omitted]\n\n", lockStat.Added, lockStat.Deleted)) } else { fileDiff, err := stagedDiffForFile(stat.Filename) if err != nil { diff --git a/internal/git/lockfile_test.go b/internal/git/lockfile_test.go index 4a94b5b..6c9c2aa 100644 --- a/internal/git/lockfile_test.go +++ b/internal/git/lockfile_test.go @@ -105,9 +105,9 @@ func TestParseNumstatBinaryFile(t *testing.T) { func TestStagedDiffWithSummary(t *testing.T) { repo := setupRepo(t) withRepo(t, repo, func() { - // Create a regular file and a lock file + // Create a regular file and a large lock file (above threshold) writeFile(t, repo, "main.go", "package main\n\nfunc main() {}\n") - writeLargeLockFile(t, repo, "uv.lock", 100) + writeLargeLockFile(t, repo, "uv.lock", 300) runGit(t, repo, "add", "main.go", "uv.lock") @@ -129,7 +129,7 @@ func TestStagedDiffWithSummary(t *testing.T) { t.Error("expected diff NOT to contain uv.lock content details") } // Should contain line count summary - if !strings.Contains(diff, "100") { + if !strings.Contains(diff, "300") { t.Error("expected diff to contain line count for uv.lock") } }) @@ -146,11 +146,11 @@ func writeLargeLockFile(t *testing.T, repo, name string, lines int) { writeFile(t, repo, name, content.String()) } -func TestStagedDiffWithSummaryOnlyLockFile(t *testing.T) { +func TestStagedDiffWithSummaryOnlyLargeLockFile(t *testing.T) { repo := setupRepo(t) withRepo(t, repo, func() { - // Only stage a lock file - writeLargeLockFile(t, repo, "package-lock.json", 50) + // Only stage a large lock file (above threshold) + writeLargeLockFile(t, repo, "package-lock.json", 300) runGit(t, repo, "add", "package-lock.json") diff, err := StagedDiffWithSummary() @@ -198,3 +198,49 @@ func TestStagedDiffWithSummaryNoLockFiles(t *testing.T) { } }) } + +func TestStagedDiffWithSummarySmallLockFile(t *testing.T) { + repo := setupRepo(t) + withRepo(t, repo, func() { + // Small lock file (below threshold) should show full diff + writeLargeLockFile(t, repo, "uv.lock", 50) + runGit(t, repo, "add", "uv.lock") + + diff, err := StagedDiffWithSummary() + if err != nil { + t.Fatalf("StagedDiffWithSummary error: %v", err) + } + + // Should contain full diff content (not summarized) + if !strings.Contains(diff, "package-version-") { + t.Error("expected small lock file to show full diff content") + } + // Should NOT contain summary marker + if strings.Contains(diff, "content omitted") { + t.Error("expected small lock file NOT to be summarized") + } + }) +} + +func TestStagedDiffWithSummaryLargeLockFile(t *testing.T) { + repo := setupRepo(t) + withRepo(t, repo, func() { + // Large lock file (above threshold) should be summarized + writeLargeLockFile(t, repo, "uv.lock", 300) + runGit(t, repo, "add", "uv.lock") + + diff, err := StagedDiffWithSummary() + if err != nil { + t.Fatalf("StagedDiffWithSummary error: %v", err) + } + + // Should contain summary marker + if !strings.Contains(diff, "content omitted") { + t.Error("expected large lock file to be summarized") + } + // Should NOT contain full content + if strings.Contains(diff, "package-version-") { + t.Error("expected large lock file NOT to show full content") + } + }) +} From 17873084d003bc361e3c661b60ba93e593253803 Mon Sep 17 00:00:00 2001 From: Yuya Fujiwara Date: Wed, 28 Jan 2026 23:19:24 +0900 Subject: [PATCH 7/7] refactor(lockfile): change threshold to bytes-based and optimize diff collection Changes the lock file summary threshold from a line-count metric to a byte-size metric (150KB) that better aligns with LLM context limits. Optimizes the diff collection logic to fetch diffs once and reuse them when determining whether a file exceeds the threshold, eliminating redundant git diff calls and improving performance. --- internal/git/lockfile.go | 56 +++++++++++++++++++++++------------ internal/git/lockfile_test.go | 37 ++++++++++++++++------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/internal/git/lockfile.go b/internal/git/lockfile.go index 65e2006..5090e60 100644 --- a/internal/git/lockfile.go +++ b/internal/git/lockfile.go @@ -20,9 +20,10 @@ var lockFileSuffixes = []string{ "-lock.yaml", } -// LockFileSummaryThreshold is the minimum number of changed lines +// LockFileSummaryThreshold is the minimum diff size in bytes // before a lock file diff is summarized instead of shown in full. -const LockFileSummaryThreshold = 200 +// 150KB is chosen based on LLM context limits (Claude Haiku: 200k tokens). +const LockFileSummaryThreshold = 150 * 1024 // 150KB func IsLockFile(filename string) bool { base := filepath.Base(filename) @@ -92,34 +93,51 @@ func StagedDiffWithSummary() (string, error) { stats := ParseNumstat(numstatOut.String()) - // Identify large lock files that should be summarized - largeLockFiles := make(map[string]FileStat) + // Get diff for each file and check if lock files exceed threshold + type fileDiffInfo struct { + stat FileStat + diff string + isLarge bool + } + fileDiffs := make([]fileDiffInfo, 0, len(stats)) + for _, stat := range stats { - if IsLockFile(stat.Filename) { - totalChanges := stat.Added + stat.Deleted - if totalChanges >= LockFileSummaryThreshold { - largeLockFiles[stat.Filename] = stat - } + diff, err := stagedDiffForFile(stat.Filename) + if err != nil { + return "", err + } + + isLarge := IsLockFile(stat.Filename) && len(diff) >= LockFileSummaryThreshold + fileDiffs = append(fileDiffs, fileDiffInfo{ + stat: stat, + diff: diff, + isLarge: isLarge, + }) + } + + // Check if any lock file exceeds threshold + hasLargeLockFile := false + for _, fd := range fileDiffs { + if fd.isLarge { + hasLargeLockFile = true + break } } // If no large lock files, return standard diff - if len(largeLockFiles) == 0 { + if !hasLargeLockFile { return StagedDiff() } var result strings.Builder - for _, stat := range stats { - if lockStat, isLargeLock := largeLockFiles[stat.Filename]; isLargeLock { - result.WriteString(fmt.Sprintf("diff --git a/%s b/%s\n", stat.Filename, stat.Filename)) - result.WriteString(fmt.Sprintf("[Lock file: +%d -%d lines, content omitted]\n\n", lockStat.Added, lockStat.Deleted)) + for _, fd := range fileDiffs { + if fd.isLarge { + result.WriteString(fmt.Sprintf("diff --git a/%s b/%s\n", fd.stat.Filename, fd.stat.Filename)) + result.WriteString(fmt.Sprintf("[Lock file: +%d -%d lines, %d bytes, content omitted]\n\n", + fd.stat.Added, fd.stat.Deleted, len(fd.diff))) } else { - fileDiff, err := stagedDiffForFile(stat.Filename) - if err != nil { - return "", err - } - result.WriteString(fileDiff) + result.WriteString(fd.diff) } } diff --git a/internal/git/lockfile_test.go b/internal/git/lockfile_test.go index 6c9c2aa..b970fd1 100644 --- a/internal/git/lockfile_test.go +++ b/internal/git/lockfile_test.go @@ -105,9 +105,9 @@ func TestParseNumstatBinaryFile(t *testing.T) { func TestStagedDiffWithSummary(t *testing.T) { repo := setupRepo(t) withRepo(t, repo, func() { - // Create a regular file and a large lock file (above threshold) + // Create a regular file and a large lock file (above 150KB threshold) writeFile(t, repo, "main.go", "package main\n\nfunc main() {}\n") - writeLargeLockFile(t, repo, "uv.lock", 300) + writeLockFileWithSize(t, repo, "uv.lock", 160*1024) // 160KB, above threshold runGit(t, repo, "add", "main.go", "uv.lock") @@ -128,13 +128,28 @@ func TestStagedDiffWithSummary(t *testing.T) { if strings.Contains(diff, "package-version-1") { t.Error("expected diff NOT to contain uv.lock content details") } - // Should contain line count summary - if !strings.Contains(diff, "300") { - t.Error("expected diff to contain line count for uv.lock") + // Should contain "content omitted" marker + if !strings.Contains(diff, "content omitted") { + t.Error("expected diff to contain summary marker") } }) } +// writeLockFileWithSize creates a lock file with approximately the specified size in bytes +func writeLockFileWithSize(t *testing.T, repo, name string, targetBytes int) { + t.Helper() + var content strings.Builder + lineSize := len("package-version-0 = \"1.0.0\"\n") + lines := targetBytes / lineSize + for i := 0; i < lines; i++ { + content.WriteString("package-version-") + content.WriteString(strconv.Itoa(i)) + content.WriteString(" = \"1.0.0\"\n") + } + writeFile(t, repo, name, content.String()) +} + +// writeLargeLockFile creates a lock file with the specified number of lines (for backward compatibility) func writeLargeLockFile(t *testing.T, repo, name string, lines int) { t.Helper() var content strings.Builder @@ -149,8 +164,8 @@ func writeLargeLockFile(t *testing.T, repo, name string, lines int) { func TestStagedDiffWithSummaryOnlyLargeLockFile(t *testing.T) { repo := setupRepo(t) withRepo(t, repo, func() { - // Only stage a large lock file (above threshold) - writeLargeLockFile(t, repo, "package-lock.json", 300) + // Only stage a large lock file (above 150KB threshold) + writeLockFileWithSize(t, repo, "package-lock.json", 160*1024) runGit(t, repo, "add", "package-lock.json") diff, err := StagedDiffWithSummary() @@ -202,8 +217,8 @@ func TestStagedDiffWithSummaryNoLockFiles(t *testing.T) { func TestStagedDiffWithSummarySmallLockFile(t *testing.T) { repo := setupRepo(t) withRepo(t, repo, func() { - // Small lock file (below threshold) should show full diff - writeLargeLockFile(t, repo, "uv.lock", 50) + // Small lock file (below 150KB threshold) should show full diff + writeLockFileWithSize(t, repo, "uv.lock", 100*1024) // 100KB, below threshold runGit(t, repo, "add", "uv.lock") diff, err := StagedDiffWithSummary() @@ -225,8 +240,8 @@ func TestStagedDiffWithSummarySmallLockFile(t *testing.T) { func TestStagedDiffWithSummaryLargeLockFile(t *testing.T) { repo := setupRepo(t) withRepo(t, repo, func() { - // Large lock file (above threshold) should be summarized - writeLargeLockFile(t, repo, "uv.lock", 300) + // Large lock file (above 150KB threshold) should be summarized + writeLockFileWithSize(t, repo, "uv.lock", 160*1024) // 160KB, above threshold runGit(t, repo, "add", "uv.lock") diff, err := StagedDiffWithSummary()