cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.68.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

354lines · modecode

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