Skip to content

Commit f3f4ad6

Browse files
fix: address cli cleanup review findings
1 parent dcc2025 commit f3f4ad6

9 files changed

Lines changed: 391 additions & 84 deletions

File tree

internal/cache/fingerprint.go

Lines changed: 188 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,27 @@ import (
1818
// For dirty git repos (~100ms): returns commitSHA:dirtyHash.
1919
// For non-git dirs: returns empty string and an error.
2020
func RepoFingerprint(dir string) (string, error) {
21-
commitSHA, err := gitOutput(dir, "rev-parse", "HEAD")
21+
commitSHA, err := gitOutputTrim(dir, "rev-parse", "HEAD")
2222
if err != nil {
2323
return "", fmt.Errorf("not a git repo: %w", err)
2424
}
2525

26-
dirty, err := gitOutput(dir, "status", "--porcelain", "--untracked-files=all")
26+
statusOut, err := gitOutputRaw(dir, "status", "--porcelain=v1", "-z", "--untracked-files=all")
2727
if err != nil {
2828
return commitSHA, nil
2929
}
30-
dirty = filterFingerprintStatus(dirty)
30+
dirtyEntries := filterFingerprintStatus(parseGitStatusZ(statusOut))
3131

32-
if dirty == "" {
32+
if len(dirtyEntries) == 0 {
3333
return commitSHA, nil
3434
}
3535

3636
// Dirty: hash tracked changes plus untracked file contents. Generated
3737
// files are filtered because they are not uploaded for analysis.
3838
h := sha256.New()
39-
fmt.Fprintf(h, "status\x00%s\x00", dirty)
40-
if err := hashDirtyFiles(h, dir, dirty); err != nil {
39+
fmt.Fprint(h, "status\x00")
40+
writeStatusEntries(h, dirtyEntries)
41+
if err := hashDirtyFiles(h, dir, dirtyEntries); err != nil {
4142
return commitSHA + ":dirty", nil
4243
}
4344
if err := hashUntrackedFiles(h, dir); err != nil {
@@ -47,106 +48,172 @@ func RepoFingerprint(dir string) (string, error) {
4748
return commitSHA + ":" + hex.EncodeToString(sum[:8]), nil
4849
}
4950

50-
func hashDirtyFiles(h io.Writer, dir, status string) error {
51-
for _, line := range strings.Split(status, "\n") {
52-
if line == "" || strings.HasPrefix(line, "?? ") {
51+
type gitStatusEntry struct {
52+
code string
53+
paths []string
54+
}
55+
56+
func parseGitStatusZ(status string) []gitStatusEntry {
57+
if status == "" {
58+
return nil
59+
}
60+
records := strings.Split(status, "\x00")
61+
entries := make([]gitStatusEntry, 0, len(records))
62+
for i := 0; i < len(records); i++ {
63+
record := records[i]
64+
if record == "" || len(record) < 4 {
5365
continue
5466
}
55-
rel := statusPath(line)
56-
if rel == "" || ignoreFingerprintPath(rel) {
57-
continue
67+
entry := gitStatusEntry{
68+
code: record[:2],
69+
paths: []string{filepath.ToSlash(record[3:])},
5870
}
59-
full := filepath.Join(dir, rel)
60-
info, err := os.Lstat(full)
61-
if os.IsNotExist(err) {
62-
fmt.Fprintf(h, "tracked\x00%s\x00deleted\x00", rel)
71+
if isRenameOrCopyStatus(entry.code) && i+1 < len(records) && records[i+1] != "" {
72+
entry.paths = append(entry.paths, filepath.ToSlash(records[i+1]))
73+
i++
74+
}
75+
entries = append(entries, entry)
76+
}
77+
return entries
78+
}
79+
80+
func filterFingerprintStatus(entries []gitStatusEntry) []gitStatusEntry {
81+
kept := make([]gitStatusEntry, 0, len(entries))
82+
for _, entry := range entries {
83+
if !statusEntryTouchesUploadablePath(entry) {
6384
continue
6485
}
65-
if err != nil {
66-
return err
86+
kept = append(kept, entry)
87+
}
88+
sort.Slice(kept, func(i, j int) bool {
89+
return statusEntryKey(kept[i]) < statusEntryKey(kept[j])
90+
})
91+
return kept
92+
}
93+
94+
func statusEntryTouchesUploadablePath(entry gitStatusEntry) bool {
95+
for _, path := range entry.paths {
96+
if path != "" && !ignoreFingerprintPath(path) {
97+
return true
98+
}
99+
}
100+
return false
101+
}
102+
103+
func statusEntryKey(entry gitStatusEntry) string {
104+
return entry.code + "\x00" + strings.Join(entry.paths, "\x00")
105+
}
106+
107+
func writeStatusEntries(h io.Writer, entries []gitStatusEntry) {
108+
for _, entry := range entries {
109+
fmt.Fprintf(h, "%s\x00", entry.code)
110+
for _, path := range entry.paths {
111+
fmt.Fprintf(h, "%s\x00", path)
67112
}
68-
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
113+
}
114+
}
115+
116+
func isRenameOrCopyStatus(code string) bool {
117+
return strings.ContainsAny(code, "RC")
118+
}
119+
120+
func isRenameStatus(code string) bool {
121+
return strings.Contains(code, "R")
122+
}
123+
124+
func hashDirtyFiles(h io.Writer, dir string, entries []gitStatusEntry) error {
125+
for _, entry := range entries {
126+
if entry.code == "??" || len(entry.paths) == 0 {
69127
continue
70128
}
71-
fmt.Fprintf(h, "tracked\x00%s\x00%d\x00", rel, info.Size())
72-
f, err := os.Open(full)
73-
if err != nil {
74-
return err
129+
130+
if isRenameOrCopyStatus(entry.code) && len(entry.paths) > 1 {
131+
newPath := entry.paths[0]
132+
oldPath := entry.paths[1]
133+
if isRenameStatus(entry.code) && !ignoreFingerprintPath(oldPath) && oldPath != newPath {
134+
fmt.Fprintf(h, "tracked\x00%s\x00deleted\x00", oldPath)
135+
}
136+
if !ignoreFingerprintPath(newPath) {
137+
if err := hashFileState(h, dir, "tracked", newPath, true); err != nil {
138+
return err
139+
}
140+
}
141+
continue
75142
}
76-
if _, err := io.Copy(h, f); err != nil {
77-
f.Close()
78-
return err
143+
144+
rel := entry.paths[0]
145+
if rel == "" || ignoreFingerprintPath(rel) {
146+
continue
79147
}
80-
if err := f.Close(); err != nil {
148+
if err := hashFileState(h, dir, "tracked", rel, true); err != nil {
81149
return err
82150
}
83-
fmt.Fprint(h, "\x00")
84151
}
85152
return nil
86153
}
87154

88155
func hashUntrackedFiles(h io.Writer, dir string) error {
89-
out, err := gitOutput(dir, "ls-files", "--others", "--exclude-standard")
156+
out, err := gitOutputRaw(dir, "ls-files", "-z", "--others", "--exclude-standard")
90157
if err != nil || out == "" {
91158
return err
92159
}
93-
files := strings.Split(out, "\n")
160+
files := splitNUL(out)
94161
sort.Strings(files)
95162
for _, rel := range files {
96163
if rel == "" {
97164
continue
98165
}
166+
rel = filepath.ToSlash(rel)
99167
if ignoreFingerprintPath(rel) {
100168
continue
101169
}
102-
full := filepath.Join(dir, rel)
103-
info, err := os.Lstat(full)
104-
if err != nil || info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
105-
continue
106-
}
107-
fmt.Fprintf(h, "untracked\x00%s\x00%d\x00", rel, info.Size())
108-
f, err := os.Open(full)
109-
if err != nil {
110-
return err
111-
}
112-
if _, err := io.Copy(h, f); err != nil {
113-
f.Close()
114-
return err
115-
}
116-
if err := f.Close(); err != nil {
170+
if err := hashFileState(h, dir, "untracked", rel, false); err != nil {
117171
return err
118172
}
119-
fmt.Fprint(h, "\x00")
120173
}
121174
return nil
122175
}
123176

124-
func filterFingerprintStatus(status string) string {
125-
var kept []string
126-
for _, line := range strings.Split(status, "\n") {
127-
line = strings.TrimRight(line, "\r")
128-
if strings.TrimSpace(line) == "" {
129-
continue
130-
}
131-
path := statusPath(line)
132-
if path == "" || ignoreFingerprintPath(path) {
133-
continue
134-
}
135-
kept = append(kept, line)
177+
func splitNUL(out string) []string {
178+
if out == "" {
179+
return nil
136180
}
137-
sort.Strings(kept)
138-
return strings.Join(kept, "\n")
181+
fields := strings.Split(out, "\x00")
182+
if len(fields) > 0 && fields[len(fields)-1] == "" {
183+
fields = fields[:len(fields)-1]
184+
}
185+
return fields
139186
}
140187

141-
func statusPath(line string) string {
142-
if len(line) < 4 {
143-
return ""
188+
func hashFileState(h io.Writer, dir, kind, rel string, missingAsDeleted bool) error {
189+
full := filepath.Join(dir, filepath.FromSlash(rel))
190+
info, err := os.Lstat(full)
191+
if os.IsNotExist(err) {
192+
if missingAsDeleted {
193+
fmt.Fprintf(h, "%s\x00%s\x00deleted\x00", kind, rel)
194+
}
195+
return nil
196+
}
197+
if err != nil {
198+
return err
144199
}
145-
path := strings.TrimSpace(line[3:])
146-
if _, after, ok := strings.Cut(path, " -> "); ok {
147-
path = after
200+
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
201+
return nil
148202
}
149-
return filepath.ToSlash(path)
203+
fmt.Fprintf(h, "%s\x00%s\x00%d\x00", kind, rel, info.Size())
204+
f, err := os.Open(full)
205+
if err != nil {
206+
return err
207+
}
208+
if _, err := io.Copy(h, f); err != nil {
209+
f.Close()
210+
return err
211+
}
212+
if err := f.Close(); err != nil {
213+
return err
214+
}
215+
fmt.Fprint(h, "\x00")
216+
return nil
150217
}
151218

152219
func ignoreFingerprintPath(path string) bool {
@@ -155,14 +222,54 @@ func ignoreFingerprintPath(path string) bool {
155222
if len(parts) == 0 {
156223
return false
157224
}
158-
if len(parts) > 1 && strings.HasPrefix(parts[0], ".") {
225+
for _, part := range parts[:len(parts)-1] {
226+
if strings.HasPrefix(part, ".") || defaultUploadSkipDir(part) {
227+
return true
228+
}
229+
}
230+
filename := parts[len(parts)-1]
231+
return isGeneratedShardPath(path) ||
232+
isSensitiveFingerprintPath(path) ||
233+
defaultUploadSkipFile(filename) ||
234+
defaultUploadSkipExtension(filename)
235+
}
236+
237+
func defaultUploadSkipDir(name string) bool {
238+
// Keep this in lockstep with internal/shards/zip.go upload exclusions.
239+
// The cache package cannot import shards because shards already imports cache.
240+
switch name {
241+
case ".git", "node_modules", "vendor", ".venv", "venv", "__pycache__", "dist", "build",
242+
".next", ".nuxt", ".cache", ".turbo", "coverage", ".nyc_output", "__snapshots__",
243+
"docs-output", ".terraform":
159244
return true
245+
default:
246+
return false
160247
}
161-
switch parts[0] {
162-
case ".supermodel", "docs-output", "node_modules", "vendor", "dist", "build", "coverage":
248+
}
249+
250+
func defaultUploadSkipFile(name string) bool {
251+
// Keep this in lockstep with internal/shards/zip.go upload exclusions.
252+
switch name {
253+
case "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "Gemfile.lock",
254+
"poetry.lock", "go.sum", "Cargo.lock":
163255
return true
256+
default:
257+
return false
258+
}
259+
}
260+
261+
func defaultUploadSkipExtension(name string) bool {
262+
// Keep this in lockstep with internal/shards/zip.go upload exclusions.
263+
ext := strings.ToLower(filepath.Ext(name))
264+
switch ext {
265+
case ".map", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".mp4", ".mp3",
266+
".wav", ".png", ".jpg", ".jpeg", ".gif", ".webp":
267+
return true
268+
default:
269+
return strings.HasSuffix(name, ".min.js") ||
270+
strings.HasSuffix(name, ".min.css") ||
271+
strings.HasSuffix(name, ".bundle.js")
164272
}
165-
return isGeneratedShardPath(path) || isSensitiveFingerprintPath(path)
166273
}
167274

168275
func isSensitiveFingerprintPath(path string) bool {
@@ -196,13 +303,21 @@ func isGeneratedShardPath(path string) bool {
196303
}
197304
}
198305

199-
// gitOutput runs a git command in dir and returns its trimmed stdout.
200-
func gitOutput(dir string, args ...string) (string, error) {
306+
func gitOutputRaw(dir string, args ...string) (string, error) {
201307
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
202308
out, err := cmd.Output()
203309
if err != nil {
204310
return "", err
205311
}
312+
return string(out), nil
313+
}
314+
315+
// gitOutputTrim runs a git command in dir and returns stdout without trailing whitespace.
316+
func gitOutputTrim(dir string, args ...string) (string, error) {
317+
out, err := gitOutputRaw(dir, args...)
318+
if err != nil {
319+
return "", err
320+
}
206321
return strings.TrimSpace(string(out)), nil
207322
}
208323

0 commit comments

Comments
 (0)