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_blame.go

224lines · modecode

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