cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.49.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_blame.go

225lines · modecode

1package discovery
2
3import (
4 "fmt"
5 "log/slog"
6 "os"
7 "regexp"
8 "strings"
9
10 "github.com/cloudflare/pint/internal/git"
11)
12
13func NewGitBlameFinder(
14 gitCmd git.CommandRunner,
15 include []*regexp.Regexp,
16 exclude []*regexp.Regexp,
17 baseBranch string,
18 maxCommits int,
19 relaxed []*regexp.Regexp,
20) GitBlameFinder {
21 return GitBlameFinder{
22 gitCmd: gitCmd,
23 include: include,
24 exclude: exclude,
25 baseBranch: baseBranch,
26 maxCommits: maxCommits,
27 relaxed: relaxed,
28 }
29}
30
31type GitBlameFinder struct {
32 gitCmd git.CommandRunner
33 include []*regexp.Regexp
34 exclude []*regexp.Regexp
35 baseBranch string
36 maxCommits int
37 relaxed []*regexp.Regexp
38}
39
40func (f GitBlameFinder) Find() (entries []Entry, err error) {
41 slog.Info("Finding all rules to check on current git branch using git blame", slog.String("base", f.baseBranch))
42
43 cr, err := git.CommitRange(f.gitCmd, f.baseBranch)
44 if err != nil {
45 return nil, fmt.Errorf("failed to get the list of commits to scan: %w", err)
46 }
47
48 slog.Debug("Got commit range from git", slog.String("from", cr.From), slog.String("to", cr.To))
49
50 if f.maxCommits > 0 && len(cr.Commits) > f.maxCommits {
51 return nil, fmt.Errorf("number of commits to check (%d) is higher than maxCommits (%d), exiting", len(cr.Commits), f.maxCommits)
52 }
53
54 out, err := f.gitCmd("log", "--reverse", "--no-merges", "--pretty=format:%H", "--name-status", cr.String())
55 if err != nil {
56 return nil, fmt.Errorf("failed to get the list of modified files from git: %w", err)
57 }
58
59 pathCommits := map[string]map[string]struct{}{}
60 var commit, msg string
61
62 for _, line := range strings.Split(string(out), "\n") {
63 parts := strings.Split(removeRedundantSpaces(line), " ")
64 if len(parts) == 1 && parts[0] != "" {
65 commit = parts[0]
66 } else if len(parts) >= 2 {
67 op := parts[0]
68 srcPath := parts[1]
69 dstPath := parts[len(parts)-1]
70 slog.Debug("Git file change", slog.String("change", parts[0]), slog.String("path", dstPath), slog.String("commit", commit))
71 if !f.isPathAllowed(dstPath) {
72 slog.Debug("Skipping file due to include/exclude rules", slog.String("path", dstPath))
73 continue
74 }
75
76 msg, err = git.CommitMessage(f.gitCmd, commit)
77 if err != nil {
78 return nil, fmt.Errorf("failed to get commit message for %s: %w", commit, err)
79 }
80 if strings.Contains(msg, "[skip ci]") {
81 slog.Info("Found a commit with '[skip ci]', skipping all checks", slog.String("commit", commit))
82 return []Entry{}, nil
83 }
84 if strings.Contains(msg, "[no ci]") {
85 slog.Info("Found a commit with '[no ci]', skipping all checks", slog.String("commit", commit))
86 return []Entry{}, nil
87 }
88
89 // ignore directories
90 if isDir, _ := isDirectoryPath(dstPath); isDir {
91 slog.Debug("Skipping directory entry change", slog.String("path", dstPath))
92 continue
93 }
94
95 if _, ok := pathCommits[dstPath]; !ok {
96 pathCommits[dstPath] = map[string]struct{}{}
97 }
98 // check if we're dealing with a rename and if so we need to
99 // rename results in pathCommits
100 if strings.HasPrefix(op, "R") {
101 if commits, ok := pathCommits[srcPath]; ok {
102 for c := range commits {
103 pathCommits[dstPath][c] = struct{}{}
104 }
105 delete(pathCommits, srcPath)
106 }
107 }
108 // check if file is being removed, if so drop it from the results
109 if strings.HasPrefix(op, "D") {
110 delete(pathCommits, srcPath)
111 continue
112 }
113 pathCommits[dstPath][commit] = struct{}{}
114 }
115 }
116
117 var lbs git.LineBlames
118 var els []Entry
119 for path, commits := range pathCommits {
120 lbs, err = git.Blame(f.gitCmd, path)
121 if err != nil {
122 return nil, fmt.Errorf("failed to run git blame for %s: %w", path, err)
123 }
124
125 allowedLines := []int{}
126 for _, lb := range lbs {
127 // skip commits that are not part of our diff
128 if _, ok := commits[lb.Commit]; !ok {
129 continue
130 }
131 allowedLines = append(allowedLines, lb.Line)
132 }
133
134 var fd *os.File
135 fd, err = os.Open(path)
136 if err != nil {
137 return nil, err
138 }
139
140 els, err = readRules(path, path, fd, !matchesAny(f.relaxed, path))
141 if err != nil {
142 fd.Close()
143 return nil, err
144 }
145 fd.Close()
146
147 for _, e := range els {
148 e.ModifiedLines = getOverlap(e.Rule.Lines(), allowedLines)
149 if len(e.ModifiedLines) == 0 && e.PathError != nil {
150 e.ModifiedLines = allowedLines
151 }
152 if isOverlap(allowedLines, e.Rule.Lines()) || isOverlap(allowedLines, e.ModifiedLines) {
153 entries = append(entries, e)
154 }
155 }
156 }
157
158 symlinks, err := addSymlinkedEntries(entries)
159 if err != nil {
160 return nil, err
161 }
162
163 for _, entry := range symlinks {
164 if f.isPathAllowed(entry.SourcePath) {
165 entries = append(entries, entry)
166 }
167 }
168
169 return entries, nil
170}
171
172func (f GitBlameFinder) isPathAllowed(path string) bool {
173 if len(f.include) == 0 && len(f.exclude) == 0 {
174 return true
175 }
176
177 for _, pattern := range f.exclude {
178 if pattern.MatchString(path) {
179 return false
180 }
181 }
182
183 for _, pattern := range f.include {
184 if pattern.MatchString(path) {
185 return true
186 }
187 }
188
189 return false
190}
191
192func removeRedundantSpaces(line string) string {
193 return strings.Join(strings.Fields(line), " ")
194}
195
196func isOverlap(a, b []int) bool {
197 for _, i := range a {
198 for _, j := range b {
199 if i == j {
200 return true
201 }
202 }
203 }
204 return false
205}
206
207func getOverlap(a, b []int) (o []int) {
208 for _, i := range a {
209 for _, j := range b {
210 if i == j {
211 o = append(o, i)
212 }
213 }
214 }
215 return o
216}
217
218func isDirectoryPath(path string) (bool, error) {
219 fileInfo, err := os.Stat(path)
220 if err != nil {
221 return false, err
222 }
223
224 return fileInfo.IsDir(), err
225}
226