cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.75.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

369lines · modecode

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