cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.65.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

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