cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.62.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

348lines · modecode

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