cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.79.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

351lines · modecode

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