cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.61.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

327lines · 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 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 slog.Debug(
110 "Rule content was not modified on HEAD, identical rule present before",
111 slog.String("name", me.after.Rule.Name()),
112 slog.String("lines", me.after.Rule.Lines.String()),
113 )
114 me.after.State = Excluded
115 me.after.ModifiedLines = []int{}
116 case me.wasMoved:
117 slog.Debug(
118 "Rule content was not modified on HEAD but the file was moved or renamed",
119 slog.String("name", me.after.Rule.Name()),
120 slog.String("lines", me.after.Rule.Lines.String()),
121 )
122 me.after.State = Moved
123 me.after.ModifiedLines = git.CountLines(change.Body.After)
124 default:
125 slog.Debug(
126 "Rule modified on HEAD branch",
127 slog.String("name", me.after.Rule.Name()),
128 slog.String("state", me.after.State.String()),
129 slog.String("path", me.after.Path.Name),
130 slog.String("ruleLines", me.after.Rule.Lines.String()),
131 slog.String("modifiedLines", output.FormatLineRangeString(me.after.ModifiedLines)),
132 )
133 me.after.State = Modified
134 me.after.ModifiedLines = commonLines(change.Body.ModifiedLines, me.after.ModifiedLines)
135 }
136 entries = append(entries, me.after)
137 case me.hasBefore && !me.hasAfter:
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 default:
153 slog.Warn(
154 "Unknown rule state",
155 slog.String("state", me.before.State.String()),
156 slog.String("path", me.before.Path.Name),
157 slog.String("modifiedLines", output.FormatLineRangeString(me.before.ModifiedLines)),
158 )
159 entries = append(entries, me.after)
160 }
161 }
162 }
163
164 symlinks, err := addSymlinkedEntries(entries)
165 if err != nil {
166 return nil, err
167 }
168
169 for _, entry := range symlinks {
170 if f.filter.IsPathAllowed(entry.Path.Name) {
171 entries = append(entries, entry)
172 }
173 }
174
175 var found bool
176 for _, entry := range entries {
177 found = false
178 if entry.State == Removed {
179 goto NEXT
180 }
181 for i, globEntry := range allEntries {
182 if entry.Path.Name == globEntry.Path.Name && entry.Rule.IsSame(globEntry.Rule) {
183 allEntries[i].State = entry.State
184 allEntries[i].ModifiedLines = entry.ModifiedLines
185 found = true
186 break
187 }
188 }
189 NEXT:
190 if !found {
191 allEntries = append(allEntries, entry)
192 }
193 }
194
195 slog.Debug("Git branch finder completed", slog.Int("count", len(allEntries)))
196 return allEntries, nil
197}
198
199func (f GitBranchFinder) shouldSkipAllChecks(changes []*git.FileChange) (bool, error) {
200 commits := map[string]struct{}{}
201 for _, change := range changes {
202 for _, commit := range change.Commits {
203 commits[commit] = struct{}{}
204 }
205 }
206
207 for commit := range commits {
208 msg, err := git.CommitMessage(f.gitCmd, commit)
209 if err != nil {
210 return false, fmt.Errorf("failed to get commit message for %s: %w", commit, err)
211 }
212 for _, comment := range []string{"[skip ci]", "[no ci]"} {
213 if strings.Contains(msg, comment) {
214 slog.Info(
215 fmt.Sprintf("Found a commit with '%s', skipping all checks", comment),
216 slog.String("commit", commit))
217 return true, nil
218 }
219 }
220 }
221
222 return false, nil
223}
224
225func commonLines(a, b []int) (common []int) {
226 for _, ai := range a {
227 if slices.Contains(b, ai) {
228 common = append(common, ai)
229 }
230 }
231 for _, bi := range b {
232 if slices.Contains(a, bi) && !slices.Contains(common, bi) {
233 common = append(common, bi)
234 }
235 }
236 return common
237}
238
239type matchedEntry struct {
240 before Entry
241 after Entry
242 hasBefore bool
243 hasAfter bool
244 isIdentical bool
245 wasMoved bool
246}
247
248func matchEntries(before, after []Entry) (ml []matchedEntry) {
249 for _, a := range after {
250 slog.Debug(
251 "Matching HEAD rule",
252 slog.String("path", a.Path.Name),
253 slog.String("source", a.Path.SymlinkTarget),
254 slog.String("name", a.Rule.Name()),
255 )
256
257 m := matchedEntry{after: a, hasAfter: true}
258 beforeSwap := make([]Entry, 0, len(before))
259 var matches []Entry
260 var matched bool
261
262 for _, b := range before {
263 if !matched && a.Rule.Name() != "" && a.Rule.IsIdentical(b.Rule) {
264 m.before = b
265 m.hasBefore = true
266 m.isIdentical = true
267 m.wasMoved = a.Path.Name != b.Path.Name
268 matched = true
269 slog.Debug(
270 "Found identical rule on before & after",
271 slog.Bool("identical", m.isIdentical),
272 slog.Bool("moved", m.wasMoved),
273 )
274 } else {
275 beforeSwap = append(beforeSwap, b)
276 }
277 }
278 before = beforeSwap
279
280 if !matched {
281 before, matches = findRulesByName(before, a.Rule.Name(), a.Rule.Type())
282 switch len(matches) {
283 case 0:
284 case 1:
285 m.before = matches[0]
286 m.hasBefore = true
287 m.wasMoved = a.Path.Name != matches[0].Path.Name
288 slog.Debug("Found rule with same name on before & after")
289 default:
290 slog.Debug(
291 "Found multiple rules with same name on before & after",
292 slog.Int("matches", len(matches)),
293 )
294 before = append(before, matches...)
295 }
296 }
297
298 ml = append(ml, m)
299 }
300
301 for _, b := range before {
302 ml = append(ml, matchedEntry{before: b, hasBefore: true})
303 }
304
305 return ml
306}
307
308func findRulesByName(entries []Entry, name string, typ parser.RuleType) (nomatch, match []Entry) {
309 for _, entry := range entries {
310 if entry.PathError == nil && entry.Rule.Type() == typ && entry.Rule.Name() == name {
311 match = append(match, entry)
312 } else {
313 nomatch = append(nomatch, entry)
314 }
315 }
316 return nomatch, match
317}
318
319func countCommits(changes []*git.FileChange) int {
320 commits := map[string]struct{}{}
321 for _, change := range changes {
322 for _, commit := range change.Commits {
323 commits[commit] = struct{}{}
324 }
325 }
326 return len(commits)
327}
328