cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.63.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

359lines · modecode

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