cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.43.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

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