cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.77.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

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