cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.50.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

295lines · modecode

1package discovery
2
3import (
4 "bytes"
5 "fmt"
6 "log/slog"
7 "regexp"
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 include []*regexp.Regexp,
20 exclude []*regexp.Regexp,
21 baseBranch string,
22 maxCommits int,
23 relaxed []*regexp.Regexp,
24) GitBranchFinder {
25 return GitBranchFinder{
26 gitCmd: gitCmd,
27 include: include,
28 exclude: exclude,
29 baseBranch: baseBranch,
30 maxCommits: maxCommits,
31 relaxed: relaxed,
32 }
33}
34
35type GitBranchFinder struct {
36 gitCmd git.CommandRunner
37 include []*regexp.Regexp
38 exclude []*regexp.Regexp
39 baseBranch string
40 maxCommits int
41 relaxed []*regexp.Regexp
42}
43
44func (f GitBranchFinder) Find() (entries []Entry, err error) {
45 slog.Info("Finding all rules to check on current git branch", slog.String("base", f.baseBranch))
46
47 cr, err := git.CommitRange(f.gitCmd, f.baseBranch)
48 if err != nil {
49 return nil, fmt.Errorf("failed to get the list of commits to scan: %w", err)
50 }
51 slog.Debug("Got commit range from git", slog.String("from", cr.From), slog.String("to", cr.To))
52
53 if len(cr.Commits) > f.maxCommits {
54 return nil, fmt.Errorf("number of commits to check (%d) is higher than maxCommits (%d), exiting", len(cr.Commits), f.maxCommits)
55 }
56
57 changes, err := git.Changes(f.gitCmd, cr)
58 if err != nil {
59 return nil, err
60 }
61
62 shouldSkip, err := f.shouldSkipAllChecks(changes)
63 if err != nil {
64 return nil, err
65 }
66 if shouldSkip {
67 return nil, nil
68 }
69
70 for _, change := range changes {
71 if !f.isPathAllowed(change.Path.After.Name) {
72 slog.Debug("Skipping file due to include/exclude rules", slog.String("path", change.Path.After.Name))
73 continue
74 }
75
76 var entriesBefore, entriesAfter []Entry
77 entriesBefore, _ = readRules(
78 change.Path.Before.EffectivePath(),
79 change.Path.Before.Name,
80 bytes.NewReader(change.Body.Before),
81 !matchesAny(f.relaxed, change.Path.Before.Name),
82 )
83 entriesAfter, err = readRules(
84 change.Path.After.EffectivePath(),
85 change.Path.After.Name,
86 bytes.NewReader(change.Body.After),
87 !matchesAny(f.relaxed, change.Path.After.Name),
88 )
89 if err != nil {
90 return nil, fmt.Errorf("invalid file syntax: %w", err)
91 }
92
93 for _, me := range matchEntries(entriesBefore, entriesAfter) {
94 switch {
95 case me.before == nil && me.after != nil:
96 me.after.State = Added
97 me.after.ModifiedLines = commonLines(change.Body.ModifiedLines, me.after.ModifiedLines)
98 slog.Debug(
99 "Rule added on HEAD branch",
100 slog.String("name", me.after.Rule.Name()),
101 slog.String("state", me.after.State.String()),
102 slog.String("path", me.after.SourcePath),
103 slog.String("ruleLines", output.FormatLineRangeString(me.after.Rule.Lines())),
104 slog.String("modifiedLines", output.FormatLineRangeString(me.after.ModifiedLines)),
105 )
106 entries = append(entries, *me.after)
107 case me.before != nil && me.after != nil:
108 if 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", output.FormatLineRangeString(me.after.Rule.Lines())),
113 )
114 continue
115 }
116 if 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", output.FormatLineRangeString(me.after.Rule.Lines())),
121 )
122 me.after.State = Moved
123 me.after.ModifiedLines = git.CountLines(change.Body.After)
124 } else {
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.SourcePath),
130 slog.String("ruleLines", output.FormatLineRangeString(me.after.Rule.Lines())),
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.before != nil && me.after == nil:
138 me.before.State = Removed
139 slog.Debug(
140 "Rule removed on HEAD branch",
141 slog.String("name", me.before.Rule.Name()),
142 slog.String("state", me.before.State.String()),
143 slog.String("path", me.before.SourcePath),
144 slog.String("ruleLines", output.FormatLineRangeString(me.before.Rule.Lines())),
145 slog.String("modifiedLines", output.FormatLineRangeString(me.before.ModifiedLines)),
146 )
147 entries = append(entries, *me.before)
148 default:
149 slog.Debug(
150 "Unknown rule",
151 slog.String("state", me.before.State.String()),
152 slog.String("path", me.before.SourcePath),
153 slog.String("modifiedLines", output.FormatLineRangeString(me.before.ModifiedLines)),
154 )
155 entries = append(entries, *me.after)
156 }
157 }
158 }
159
160 symlinks, err := addSymlinkedEntries(entries)
161 if err != nil {
162 return nil, err
163 }
164
165 for _, entry := range symlinks {
166 if f.isPathAllowed(entry.SourcePath) {
167 entries = append(entries, entry)
168 }
169 }
170
171 return entries, nil
172}
173
174func (f GitBranchFinder) isPathAllowed(path string) bool {
175 if len(f.include) == 0 && len(f.exclude) == 0 {
176 return true
177 }
178
179 for _, pattern := range f.exclude {
180 if pattern.MatchString(path) {
181 return false
182 }
183 }
184
185 for _, pattern := range f.include {
186 if pattern.MatchString(path) {
187 return true
188 }
189 }
190
191 return false
192}
193
194func (f GitBranchFinder) shouldSkipAllChecks(changes []*git.FileChange) (bool, error) {
195 commits := map[string]struct{}{}
196 for _, change := range changes {
197 for _, commit := range change.Commits {
198 commits[commit] = struct{}{}
199 }
200 }
201
202 for commit := range commits {
203 msg, err := git.CommitMessage(f.gitCmd, commit)
204 if err != nil {
205 return false, fmt.Errorf("failed to get commit message for %s: %w", commit, err)
206 }
207 for _, comment := range []string{"[skip ci]", "[no ci]"} {
208 if strings.Contains(msg, comment) {
209 slog.Info(
210 fmt.Sprintf("Found a commit with '%s', skipping all checks", comment),
211 slog.String("commit", commit))
212 return true, nil
213 }
214 }
215 }
216
217 return false, nil
218}
219
220func commonLines(a, b []int) (common []int) {
221 for _, ai := range a {
222 if slices.Contains(b, ai) {
223 common = append(common, ai)
224 }
225 }
226 for _, bi := range b {
227 if slices.Contains(a, bi) && !slices.Contains(common, bi) {
228 common = append(common, bi)
229 }
230 }
231 return common
232}
233
234type matchedEntry struct {
235 before *Entry
236 after *Entry
237 isIdentical bool
238 wasMoved bool
239}
240
241func matchEntries(before, after []Entry) (ml []matchedEntry) {
242 for _, a := range after {
243 a := a
244
245 m := matchedEntry{after: &a}
246 beforeSwap := make([]Entry, 0, len(before))
247 var matches []Entry
248 var matched bool
249
250 for _, b := range before {
251 b := b
252
253 if !matched && a.Rule.Name() != "" && a.Rule.IsIdentical(b.Rule) {
254 m.before = &b
255 m.isIdentical = true
256 m.wasMoved = a.SourcePath != b.SourcePath
257 matched = true
258 } else {
259 beforeSwap = append(beforeSwap, b)
260 }
261 }
262 before = beforeSwap
263
264 if !matched {
265 before, matches = findRulesByName(before, a.Rule.Name(), a.Rule.Type())
266 switch len(matches) {
267 case 0:
268 case 1:
269 m.before = &matches[0]
270 default:
271 before = append(before, matches...)
272 }
273 }
274
275 ml = append(ml, m)
276 }
277
278 for _, b := range before {
279 b := b
280 ml = append(ml, matchedEntry{before: &b})
281 }
282
283 return ml
284}
285
286func findRulesByName(entries []Entry, name string, typ parser.RuleType) (nomatch, match []Entry) {
287 for _, entry := range entries {
288 if entry.PathError == nil && entry.Rule.Type() == typ && entry.Rule.Name() == name {
289 match = append(match, entry)
290 } else {
291 nomatch = append(nomatch, entry)
292 }
293 }
294 return nomatch, match
295}
296