cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.60.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

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