cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.57.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_branch.go

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