cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.42.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/git_blame.go

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