cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.44.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

274lines · modecode

1package discovery
2
3import (
4 "bytes"
5 "fmt"
6 "regexp"
7 "strings"
8
9 "github.com/rs/zerolog/log"
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 cr, err := git.CommitRange(f.gitCmd, f.baseBranch)
46 if err != nil {
47 return nil, fmt.Errorf("failed to get the list of commits to scan: %w", err)
48 }
49 log.Debug().Str("from", cr.From).Str("to", cr.To).Msg("Got commit range from git")
50
51 if len(cr.Commits) > f.maxCommits {
52 return nil, fmt.Errorf("number of commits to check (%d) is higher than maxCommits (%d), exiting", len(cr.Commits), f.maxCommits)
53 }
54
55 changes, err := git.Changes(f.gitCmd, cr)
56 if err != nil {
57 return nil, err
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 if !f.isPathAllowed(change.Path.After.Name) {
70 log.Debug().Str("path", change.Path.After.Name).Msg("Skipping file due to include/exclude rules")
71 continue
72 }
73
74 var entriesBefore, entriesAfter []Entry
75 entriesBefore, _ = readRules(
76 change.Path.Before.EffectivePath(),
77 change.Path.Before.Name,
78 bytes.NewReader(change.Body.Before),
79 !matchesAny(f.relaxed, change.Path.Before.Name),
80 )
81 entriesAfter, err = readRules(
82 change.Path.After.EffectivePath(),
83 change.Path.After.Name,
84 bytes.NewReader(change.Body.After),
85 !matchesAny(f.relaxed, change.Path.After.Name),
86 )
87 if err != nil {
88 return nil, fmt.Errorf("invalid file syntax: %w", err)
89 }
90
91 for _, me := range matchEntries(entriesBefore, entriesAfter) {
92 switch {
93 case me.before == nil && me.after != nil:
94 me.after.State = Added
95 me.after.ModifiedLines = commonLines(change.Body.ModifiedLines, me.after.ModifiedLines)
96 log.Debug().
97 Str("name", me.after.Rule.Name()).
98 Stringer("state", me.after.State).
99 Str("path", me.after.SourcePath).
100 Str("ruleLines", output.FormatLineRangeString(me.after.Rule.Lines())).
101 Str("modifiedLines", output.FormatLineRangeString(me.after.ModifiedLines)).
102 Msg("Rule added on HEAD branch")
103 entries = append(entries, *me.after)
104 case me.before != nil && me.after != nil:
105 if me.isIdentical {
106 log.Debug().
107 Str("name", me.after.Rule.Name()).
108 Str("lines", output.FormatLineRangeString(me.after.Rule.Lines())).
109 Msg("Rule content was not modified on HEAD, identical rule present before")
110 continue
111 }
112 me.after.State = Modified
113 me.after.ModifiedLines = commonLines(change.Body.ModifiedLines, me.after.ModifiedLines)
114 log.Debug().
115 Str("name", me.after.Rule.Name()).
116 Stringer("state", me.after.State).
117 Str("path", me.after.SourcePath).
118 Str("ruleLines", output.FormatLineRangeString(me.after.Rule.Lines())).
119 Str("modifiedLines", output.FormatLineRangeString(me.after.ModifiedLines)).
120 Msg("Rule modified on HEAD branch")
121 entries = append(entries, *me.after)
122 case me.before != nil && me.after == nil:
123 me.before.State = Removed
124 log.Debug().
125 Str("name", me.before.Rule.Name()).
126 Stringer("state", me.before.State).
127 Str("path", me.before.SourcePath).
128 Str("ruleLines", output.FormatLineRangeString(me.before.Rule.Lines())).
129 Str("modifiedLines", output.FormatLineRangeString(me.before.ModifiedLines)).
130 Msg("Rule removed on HEAD branch")
131 entries = append(entries, *me.before)
132 default:
133 log.Debug().
134 Stringer("state", me.before.State).
135 Str("path", me.before.SourcePath).
136 Str("modifiedLines", output.FormatLineRangeString(me.before.ModifiedLines)).
137 Msg("Unknown rule")
138 entries = append(entries, *me.after)
139 }
140 }
141 }
142
143 symlinks, err := addSymlinkedEntries(entries)
144 if err != nil {
145 return nil, err
146 }
147
148 for _, entry := range symlinks {
149 if f.isPathAllowed(entry.SourcePath) {
150 entries = append(entries, entry)
151 }
152 }
153
154 return entries, nil
155}
156
157func (f GitBranchFinder) isPathAllowed(path string) bool {
158 if len(f.include) == 0 && len(f.exclude) == 0 {
159 return true
160 }
161
162 for _, pattern := range f.exclude {
163 if pattern.MatchString(path) {
164 return false
165 }
166 }
167
168 for _, pattern := range f.include {
169 if pattern.MatchString(path) {
170 return true
171 }
172 }
173
174 return false
175}
176
177func (f GitBranchFinder) shouldSkipAllChecks(changes []*git.FileChange) (bool, error) {
178 commits := map[string]struct{}{}
179 for _, change := range changes {
180 for _, commit := range change.Commits {
181 commits[commit] = struct{}{}
182 }
183 }
184
185 for commit := range commits {
186 msg, err := git.CommitMessage(f.gitCmd, commit)
187 if err != nil {
188 return false, fmt.Errorf("failed to get commit message for %s: %w", commit, err)
189 }
190 for _, comment := range []string{"[skip ci]", "[no ci]"} {
191 if strings.Contains(msg, comment) {
192 log.Info().Str("commit", commit).Msgf("Found a commit with '%s', skipping all checks", comment)
193 return true, nil
194 }
195 }
196 }
197
198 return false, nil
199}
200
201func commonLines(a, b []int) (common []int) {
202 for _, ai := range a {
203 if slices.Contains(b, ai) {
204 common = append(common, ai)
205 }
206 }
207 for _, bi := range b {
208 if slices.Contains(a, bi) && !slices.Contains(common, bi) {
209 common = append(common, bi)
210 }
211 }
212 return common
213}
214
215type matchedEntry struct {
216 before *Entry
217 after *Entry
218 isIdentical bool
219}
220
221func matchEntries(before, after []Entry) (ml []matchedEntry) {
222 for _, a := range after {
223 a := a
224
225 m := matchedEntry{after: &a}
226 beforeSwap := make([]Entry, 0, len(before))
227 var matches []Entry
228 var matched bool
229
230 for _, b := range before {
231 b := b
232
233 if !matched && a.Rule.Name() != "" && a.Rule.ToYAML() == b.Rule.ToYAML() {
234 m.before = &b
235 m.isIdentical = true
236 matched = true
237 } else {
238 beforeSwap = append(beforeSwap, b)
239 }
240 }
241 before = beforeSwap
242
243 if !matched {
244 before, matches = findRulesByName(before, a.Rule.Name(), a.Rule.Type())
245 switch len(matches) {
246 case 0:
247 case 1:
248 m.before = &matches[0]
249 default:
250 before = append(before, matches...)
251 }
252 }
253
254 ml = append(ml, m)
255 }
256
257 for _, b := range before {
258 b := b
259 ml = append(ml, matchedEntry{before: &b})
260 }
261
262 return ml
263}
264
265func findRulesByName(entries []Entry, name string, typ parser.RuleType) (nomatch, match []Entry) {
266 for _, entry := range entries {
267 if entry.PathError == nil && entry.Rule.Type() == typ && entry.Rule.Name() == name {
268 match = append(match, entry)
269 } else {
270 nomatch = append(nomatch, entry)
271 }
272 }
273 return nomatch, match
274}