-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommit_deltas.go
More file actions
180 lines (166 loc) · 5.52 KB
/
commit_deltas.go
File metadata and controls
180 lines (166 loc) · 5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package repomap
import (
"context"
"os"
"path/filepath"
"slices"
)
// computeSymbolDeltas returns a per-file diff of symbols between HEAD and the
// working tree. Uses the real language parsers (via parseDirtyFiles dispatch)
// for both pre and post, enabling signature-aware Modified detection.
// Missing-at-HEAD is treated as an all-added file.
//
// Parse-failure guard: when the post-side parser returns zero symbols for a
// file that still exists on disk and had pre-symbols at HEAD, the result is
// treated as a parse failure (not "everything removed"). The file's path is
// appended to skipped and no delta is emitted for it. This prevents a
// transiently-malformed file (e.g. mid-write by a formatter hook) from
// producing a bogus all-removed delta that promotes the change to breaking.
func computeSymbolDeltas(ctx context.Context, root string, files []fileChange, postSymbols map[string]*FileSymbols) (map[string]symbolDelta, []string) {
out := make(map[string]symbolDelta, len(files))
var skipped []string
for _, f := range files {
if f.Language == "" {
continue
}
post := postSymbols[f.Path]
postSigs := symbolSigMap(post)
// Pure addition — no HEAD content; every post-symbol is "added".
if f.IndexStatus == "A" && f.Status == "A" {
var added []string
for name := range postSigs {
added = append(added, name)
}
slices.Sort(added)
if len(added) > 0 {
out[f.Path] = symbolDelta{Path: f.Path, Added: added}
}
continue
}
// Read HEAD content and parse with the same dispatcher used for post.
var preSigs map[string]string // name → signature
var preExported map[string]bool
preSrc, _ := gitShowAt(ctx, root, "HEAD", oldPathOr(f))
if preSrc != "" {
preFS := parseFileSymbolsFromSource(root, f.Path, f.Language, preSrc)
preSigs = symbolSigMap(preFS)
preExported = symbolExportedMap(preFS)
}
// Parse-failure guard. If pre had symbols, the file is not a deletion,
// and post is absent from the map (parseDirtyFiles only inserts on
// successful non-nil parse), the post parser failed — transient
// mid-write, partial save, or tree-sitter recovery with no matched
// declarations. Refuse to claim "everything was removed" — skip and
// surface via skipped. Note: post==nil means absent (parse failed);
// post!=nil with empty Symbols means the file genuinely has no symbols.
_, postPresent := postSymbols[f.Path]
if len(preSigs) > 0 && !postPresent && f.Status != "D" && f.IndexStatus != "D" {
skipped = append(skipped, f.Path)
continue
}
postExported := symbolExportedMap(post)
var added, removed, modified []string
for name, postSig := range postSigs {
if preSig, exists := preSigs[name]; !exists {
added = append(added, name)
} else if preSig != postSig {
modified = append(modified, name)
}
}
for name := range preSigs {
if _, exists := postSigs[name]; !exists {
removed = append(removed, name)
}
}
slices.Sort(added)
slices.Sort(removed)
slices.Sort(modified)
if len(added) == 0 && len(removed) == 0 && len(modified) == 0 {
continue
}
// Breaking if any removed symbol was exported, or any modified symbol
// was exported in both pre and post (public API signature change).
breaking := false
for _, name := range removed {
if preExported[name] {
breaking = true
break
}
}
if !breaking {
for _, name := range modified {
if preExported[name] && postExported[name] {
breaking = true
break
}
}
}
out[f.Path] = symbolDelta{
Path: f.Path,
Added: added,
Removed: removed,
Modified: modified,
Breaking: breaking,
}
}
return out, skipped
}
// oldPathOr returns OldPath for renames, else Path. Needed so `git show HEAD:`
// resolves to the file's pre-rename name.
func oldPathOr(f fileChange) string {
if f.OldPath != "" {
return f.OldPath
}
return f.Path
}
// symbolSigMap returns a map of symbol name → signature for all symbols in fs.
// Returns an empty map when fs is nil (new/unparsable file). When two symbols
// share a name (overloads), the last one wins — commit-message diffing only
// needs to detect that a name was added, removed, or changed.
func symbolSigMap(fs *FileSymbols) map[string]string {
out := make(map[string]string)
if fs == nil {
return out
}
for _, s := range fs.Symbols {
out[s.Name] = s.Signature
}
return out
}
// symbolExportedMap returns a map of symbol name → whether it is publicly
// exported (Symbol.Exported=true). PHP public visibility is already folded
// into Exported by phpVisibilityToExported. Used for breaking-change detection.
func symbolExportedMap(fs *FileSymbols) map[string]bool {
out := make(map[string]bool)
if fs == nil {
return out
}
for _, s := range fs.Symbols {
if s.Exported {
out[s.Name] = true
}
}
return out
}
// parseFileSymbolsFromSource parses an in-memory source string (typically HEAD
// content from `git show`) through the same ladder as working-tree files:
// Go AST for Go, then parseNonGoFile (tree-sitter → regex) for everything
// else. Writes src to a temp file because the on-disk parsers require a path.
// Returns nil on parse failure.
func parseFileSymbolsFromSource(root, path, language, src string) *FileSymbols {
tmp, err := os.CreateTemp("", "repomap-presym-*"+filepath.Ext(path))
if err != nil {
return nil
}
defer os.Remove(tmp.Name())
if _, err := tmp.WriteString(src); err != nil {
tmp.Close()
return nil
}
tmp.Close()
if language == "go" {
fs, _ := ParseGoFile(tmp.Name(), root)
return fs
}
return parseNonGoFile(tmp.Name(), root, language)
}