cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.80.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

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