cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.81.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

341lines · modecode

1package discovery
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "log/slog"
8 "regexp"
9 "slices"
10 "sort"
11 "strings"
12
13 "github.com/cloudflare/pint/internal/git"
14 "github.com/cloudflare/pint/internal/parser"
15)
16
17func NewGitBranchFinder(
18 gitCmd git.CommandRunner,
19 filter git.PathFilter,
20 baseBranch string,
21 maxCommits int,
22 opts parser.Options,
23 allowedOwners []*regexp.Regexp,
24) GitBranchFinder {
25 return GitBranchFinder{
26 gitCmd: gitCmd,
27 filter: filter,
28 baseBranch: baseBranch,
29 maxCommits: maxCommits,
30 opts: opts,
31 allowedOwners: allowedOwners,
32 }
33}
34
35type GitBranchFinder struct {
36 gitCmd git.CommandRunner
37 baseBranch string
38 filter git.PathFilter
39 allowedOwners []*regexp.Regexp
40 maxCommits int
41 opts parser.Options
42}
43
44func (f GitBranchFinder) Find(allEntries []*Entry) (entries []*Entry, err error) {
45 changes, err := git.Changes(f.gitCmd, f.baseBranch, f.filter)
46 if err != nil {
47 return nil, err
48 }
49
50 if totalCommits := countCommits(changes); totalCommits > f.maxCommits {
51 return nil, fmt.Errorf("number of commits to check (%d) is higher than maxCommits (%d), exiting", totalCommits, f.maxCommits)
52 }
53
54 shouldSkip, err := f.shouldSkipAllChecks(changes)
55 if err != nil {
56 return nil, err
57 }
58 if shouldSkip {
59 return nil, nil
60 }
61
62 for _, change := range changes {
63 p := parser.NewParser(f.opts.WithStrict(!f.filter.IsRelaxed(change.Path.Before.Name)))
64 var oldPath string
65 if change.Path.Before.Name != change.Path.After.Name {
66 oldPath = change.Path.Before.Name
67 }
68
69 var fileChanges *Changes
70 if len(change.Body.Lines) > 0 || oldPath != "" {
71 fileChanges = &Changes{Lines: change.Body.Lines, OldPath: oldPath}
72 }
73
74 var entriesBefore, entriesAfter []*Entry
75 entriesBefore = readRules(
76 change.Path.Before.EffectivePath(),
77 change.Path.Before.Name,
78 bytes.NewReader(change.Body.Before),
79 p,
80 nil,
81 fileChanges,
82 )
83 entriesAfter = readRules(
84 change.Path.After.EffectivePath(),
85 change.Path.After.Name,
86 bytes.NewReader(change.Body.After),
87 p,
88 f.allowedOwners,
89 fileChanges,
90 )
91
92 failedEntries := entriesWithPathErrors(entriesAfter)
93
94 slog.LogAttrs(
95 context.Background(), slog.LevelDebug,
96 "Parsing git file change",
97 slog.Any("commits", change.Commits),
98 slog.String("before.path", change.Path.Before.Name),
99 slog.String("before.target", change.Path.Before.SymlinkTarget),
100 slog.Any("before.type", change.Path.Before.Type),
101 slog.Int("before.entries", len(entriesBefore)),
102 slog.String("after.path", change.Path.After.Name),
103 slog.String("after.target", change.Path.After.SymlinkTarget),
104 slog.Any("after.type", change.Path.After.Type),
105 slog.Int("after.entries", len(entriesAfter)),
106 slog.Any("modifiedLines", change.Body.Lines),
107 )
108 for _, me := range matchEntries(entriesBefore, entriesAfter) {
109 switch {
110 case !me.hasBefore && me.hasAfter:
111 me.after.State = Added
112 slog.LogAttrs(
113 context.Background(), slog.LevelDebug,
114 "Rule added on HEAD branch",
115 slog.String("name", me.after.Rule.Name()),
116 slog.String("state", me.after.State.String()),
117 slog.String("path", me.after.Path.Name),
118 slog.Any("ruleLines", me.after.Rule.Lines),
119 )
120 entries = append(entries, me.after)
121 case me.hasBefore && me.hasAfter:
122 switch {
123 case me.isIdentical && !me.wasMoved:
124 me.after.State = Noop
125 slog.LogAttrs(
126 context.Background(), slog.LevelDebug,
127 "Rule content was not modified on HEAD, identical rule present before",
128 slog.String("name", me.after.Rule.Name()),
129 slog.Any("lines", me.after.Rule.Lines),
130 )
131 case me.wasMoved:
132 me.after.State = Moved
133 slog.LogAttrs(
134 context.Background(), slog.LevelDebug,
135 "Rule content was not modified on HEAD but the file was moved or renamed",
136 slog.String("name", me.after.Rule.Name()),
137 slog.Any("lines", me.after.Rule.Lines),
138 )
139 default:
140 me.after.State = Modified
141 slog.LogAttrs(
142 context.Background(), slog.LevelDebug,
143 "Rule modified on HEAD branch",
144 slog.String("name", me.after.Rule.Name()),
145 slog.String("state", me.after.State.String()),
146 slog.String("path", me.after.Path.Name),
147 slog.Any("ruleLines", me.after.Rule.Lines),
148 )
149 }
150 entries = append(entries, me.after)
151 case me.hasBefore && !me.hasAfter && len(failedEntries) == 0:
152 me.before.State = Removed
153 slog.LogAttrs(
154 context.Background(), slog.LevelDebug,
155 "Rule removed on HEAD branch",
156 slog.String("name", me.before.Rule.Name()),
157 slog.String("state", me.before.State.String()),
158 slog.String("path", me.before.Path.Name),
159 slog.Any("ruleLines", me.before.Rule.Lines),
160 )
161 entries = append(entries, me.before)
162 case me.hasBefore && !me.hasAfter && len(failedEntries) > 0:
163 slog.LogAttrs(
164 context.Background(), slog.LevelDebug,
165 "Rule not present on HEAD branch but there are parse errors",
166 slog.String("name", me.before.Rule.Name()),
167 slog.String("state", me.before.State.String()),
168 slog.String("path", me.before.Path.Name),
169 slog.Any("ruleLines", me.before.Rule.Lines),
170 )
171 }
172 }
173 }
174
175 for _, entry := range addSymlinkedEntries(entries) {
176 if f.filter.IsPathAllowed(entry.Path.Name) {
177 entries = append(entries, entry)
178 }
179 }
180
181 var found bool
182 for _, entry := range entries {
183 found = false
184 if entry.State == Removed {
185 goto NEXT
186 }
187 for i, globEntry := range allEntries {
188 if entry.Path.Name == globEntry.Path.Name && entry.Rule.IsSame(globEntry.Rule) {
189 allEntries[i].State = entry.State
190 allEntries[i].Changes = entry.Changes
191 found = true
192 break
193 }
194 }
195 NEXT:
196 if !found {
197 allEntries = append(allEntries, entry)
198 }
199 }
200
201 slog.LogAttrs(context.Background(), slog.LevelDebug, "Git branch finder completed", slog.Int("count", len(allEntries)))
202 return allEntries, nil
203}
204
205func (f GitBranchFinder) shouldSkipAllChecks(changes []*git.FileChange) (bool, error) {
206 commits := map[string]struct{}{}
207 for _, change := range changes {
208 for _, commit := range change.Commits {
209 commits[commit] = struct{}{}
210 }
211 }
212
213 for commit := range commits {
214 msg, err := git.CommitMessage(f.gitCmd, commit)
215 if err != nil {
216 return false, fmt.Errorf("failed to get commit message for %s: %w", commit, err)
217 }
218 for _, comment := range []string{"[skip ci]", "[no ci]"} {
219 if strings.Contains(msg, comment) {
220 slog.LogAttrs(context.Background(), slog.LevelInfo,
221 fmt.Sprintf("Found a commit with '%s', skipping all checks", comment),
222 slog.String("commit", commit))
223 return true, nil
224 }
225 }
226 }
227
228 return false, nil
229}
230
231type matchedEntry struct {
232 before *Entry
233 after *Entry
234 hasBefore bool
235 hasAfter bool
236 isIdentical bool
237 wasMoved bool
238}
239
240func matchEntries(before, after []*Entry) (ml []matchedEntry) {
241 for _, a := range after {
242 slog.LogAttrs(
243 context.Background(), slog.LevelDebug,
244 "Matching HEAD rule",
245 slog.String("path", a.Path.Name),
246 slog.String("source", a.Path.SymlinkTarget),
247 slog.String("name", a.Rule.Name()),
248 )
249
250 m := matchedEntry{after: a, hasAfter: true} // nolint: exhaustruct
251 beforeSwap := make([]*Entry, 0, len(before))
252 var matches []*Entry
253 var matched bool
254
255 for _, b := range before {
256 if !matched && a.Rule.Name() != "" && a.Rule.IsIdentical(b.Rule) {
257 m.before = b
258 m.hasBefore = true
259 m.isIdentical = isEntryIdentical(b, a)
260 m.wasMoved = a.Path.Name != b.Path.Name
261 matched = true
262 slog.LogAttrs(
263 context.Background(), slog.LevelDebug,
264 "Found identical rule on before & after",
265 slog.Bool("identical", m.isIdentical),
266 slog.Bool("moved", m.wasMoved),
267 )
268 } else {
269 beforeSwap = append(beforeSwap, b)
270 }
271 }
272 before = beforeSwap
273
274 if !matched {
275 before, matches = findRulesByName(before, a.Rule.Name(), a.Rule.Type())
276 switch len(matches) {
277 case 0:
278 case 1:
279 m.before = matches[0]
280 m.hasBefore = true
281 m.wasMoved = a.Path.Name != matches[0].Path.Name
282 slog.LogAttrs(context.Background(), slog.LevelDebug, "Found rule with same name on before & after")
283 default:
284 slog.LogAttrs(
285 context.Background(), slog.LevelDebug,
286 "Found multiple rules with same name on before & after",
287 slog.Int("matches", len(matches)),
288 )
289 before = append(before, matches...)
290 }
291 }
292
293 ml = append(ml, m)
294 }
295
296 for _, b := range before {
297 ml = append(ml, matchedEntry{before: b, hasBefore: true}) // nolint: exhaustruct
298 }
299
300 return ml
301}
302
303func isEntryIdentical(b, a *Entry) bool {
304 if !slices.Equal(sort.StringSlice(b.DisabledChecks), sort.StringSlice(a.DisabledChecks)) {
305 slog.LogAttrs(context.Background(), slog.LevelDebug, "List of disabled checks was modified",
306 slog.Any("before", sort.StringSlice(b.DisabledChecks)),
307 slog.Any("after", sort.StringSlice(a.DisabledChecks)))
308 return false
309 }
310 return true
311}
312
313func findRulesByName(entries []*Entry, name string, ruleType parser.RuleType) (nomatch, match []*Entry) {
314 for _, entry := range entries {
315 if entry.PathError == nil && entry.Rule.Type() == ruleType && entry.Rule.Name() == name {
316 match = append(match, entry)
317 } else {
318 nomatch = append(nomatch, entry)
319 }
320 }
321 return nomatch, match
322}
323
324func countCommits(changes []*git.FileChange) int {
325 commits := map[string]struct{}{}
326 for _, change := range changes {
327 for _, commit := range change.Commits {
328 commits[commit] = struct{}{}
329 }
330 }
331 return len(commits)
332}
333
334func entriesWithPathErrors(entries []*Entry) (match []*Entry) {
335 for _, entry := range entries {
336 if entry.PathError != nil {
337 match = append(match, entry)
338 }
339 }
340 return match
341}
342