Skip to content
Closed
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
2 changes: 1 addition & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
157 changes: 157 additions & 0 deletions internal/git/lockfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package git

import (
"bytes"
"fmt"
"os/exec"
"path/filepath"
"strconv"
"strings"
)

// lockFileNames contains lock files that don't match common suffixes
var lockFileNames = map[string]bool{
"go.sum": true,
}

var lockFileSuffixes = []string{
".lock",
"-lock.json",
"-lock.yaml",
}

// LockFileSummaryThreshold is the minimum diff size in bytes
// before a lock file diff is summarized instead of shown in full.
// 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)

if lockFileNames[base] {
return true
}

lower := strings.ToLower(base)
for _, suffix := range lockFileSuffixes {
if strings.HasSuffix(lower, suffix) {
return true
}
}

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())

// 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 {
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 !hasLargeLockFile {
return StagedDiff()
}

var result strings.Builder

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 {
result.WriteString(fd.diff)
}
}

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
}
Loading