1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package predicate
16
17 import (
18 "bytes"
19 "context"
20 "fmt"
21 "strconv"
22
23 "github.com/pkg/errors"
24
25 "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/pull"
26
27 "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/common"
28 )
29
30 type ChangedFiles struct {
31 Paths []common.Regexp `yaml:"paths,omitempty"`
32 IgnorePaths []common.Regexp `yaml:"ignore,omitempty"`
33 }
34
35 var _ Predicate = &ChangedFiles{}
36
37 func (pred *ChangedFiles) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
38 var paths, ignorePaths []string
39
40 for _, path := range pred.Paths {
41 paths = append(paths, path.String())
42 }
43
44 for _, ignorePath := range pred.IgnorePaths {
45 ignorePaths = append(ignorePaths, ignorePath.String())
46 }
47
48 predicateResult := common.PredicateResult{
49 ValuePhrase: "changed files",
50 ConditionPhrase: "match",
51 ConditionsMap: map[string][]string{
52 "path patterns": paths,
53 "while ignoring": ignorePaths,
54 },
55 }
56
57 files, err := prctx.ChangedFiles()
58 if err != nil {
59 return nil, errors.Wrap(err, "failed to list changed files")
60 }
61
62 changedFiles := []string{}
63
64 for _, f := range files {
65
66 changedFiles = append(changedFiles, f.Filename)
67
68 if anyMatches(pred.IgnorePaths, f.Filename) {
69 continue
70 }
71
72 if anyMatches(pred.Paths, f.Filename) {
73 predicateResult.Values = []string{f.Filename}
74 predicateResult.Description = f.Filename + " was changed"
75 predicateResult.Satisfied = true
76 return &predicateResult, nil
77 }
78 }
79
80 predicateResult.Values = changedFiles
81 predicateResult.Description = "No changed files match the required patterns"
82 predicateResult.Satisfied = false
83 return &predicateResult, nil
84 }
85
86 func (pred *ChangedFiles) Trigger() common.Trigger {
87 return common.TriggerCommit
88 }
89
90 type OnlyChangedFiles struct {
91 Paths []common.Regexp `yaml:"paths,omitempty"`
92 }
93
94 var _ Predicate = &OnlyChangedFiles{}
95
96 func (pred *OnlyChangedFiles) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
97 var paths []string
98
99 for _, path := range pred.Paths {
100 paths = append(paths, path.String())
101 }
102
103 predicateResult := common.PredicateResult{
104 ValuePhrase: "changed files",
105 ConditionPhrase: "all match patterns",
106 ConditionValues: paths,
107 }
108
109 files, err := prctx.ChangedFiles()
110 if err != nil {
111 return nil, errors.Wrap(err, "failed to list changed files")
112 }
113
114 changedFiles := []string{}
115
116 for _, f := range files {
117
118 changedFiles = append(changedFiles, f.Filename)
119
120 if anyMatches(pred.Paths, f.Filename) {
121 continue
122 }
123 predicateResult.Values = []string{f.Filename}
124 predicateResult.Description = "A changed file does not match the required pattern"
125 predicateResult.Satisfied = false
126 return &predicateResult, nil
127 }
128
129 filesChanged := len(files) > 0
130
131 desc := ""
132 if !filesChanged {
133 desc = "No files changed"
134 }
135
136 predicateResult.Values = changedFiles
137 predicateResult.Description = desc
138 predicateResult.Satisfied = filesChanged
139 return &predicateResult, nil
140 }
141
142 func (pred *OnlyChangedFiles) Trigger() common.Trigger {
143 return common.TriggerCommit
144 }
145
146 type ModifiedLines struct {
147 Additions ComparisonExpr `yaml:"additions"`
148 Deletions ComparisonExpr `yaml:"deletions"`
149 Total ComparisonExpr `yaml:"total"`
150 }
151
152 type CompareOp uint8
153
154 const (
155 OpNone CompareOp = iota
156 OpLessThan
157 OpGreaterThan
158 )
159
160 type ComparisonExpr struct {
161 Op CompareOp
162 Value int64
163 }
164
165 func (exp ComparisonExpr) IsEmpty() bool {
166 return exp.Op == OpNone && exp.Value == 0
167 }
168
169 func (exp ComparisonExpr) Evaluate(n int64) bool {
170 switch exp.Op {
171 case OpLessThan:
172 return n < exp.Value
173 case OpGreaterThan:
174 return n > exp.Value
175 }
176 return false
177 }
178
179 func (exp ComparisonExpr) MarshalText() ([]byte, error) {
180 if exp.Op == OpNone {
181 return nil, nil
182 }
183
184 var op string
185 switch exp.Op {
186 case OpLessThan:
187 op = "<"
188 case OpGreaterThan:
189 op = ">"
190 default:
191 return nil, errors.Errorf("unknown operation: %d", exp.Op)
192 }
193 return []byte(fmt.Sprintf("%s %d", op, exp.Value)), nil
194 }
195
196 func (exp ComparisonExpr) String() string {
197 res, err := exp.MarshalText()
198 if err != nil {
199 return fmt.Sprintf("?? (op:%d) %d", exp.Op, exp.Value)
200 }
201 return string(res[:])
202 }
203
204 func (exp *ComparisonExpr) UnmarshalText(text []byte) error {
205 text = bytes.TrimSpace(text)
206 if len(text) == 0 {
207 *exp = ComparisonExpr{}
208 return nil
209 }
210
211 i := 0
212 var op CompareOp
213 switch text[i] {
214 case '<':
215 op = OpLessThan
216 case '>':
217 op = OpGreaterThan
218 default:
219 return errors.Errorf("invalid comparison operator: %c", text[i])
220 }
221
222 i++
223 for i < len(text) && (text[i] == ' ' || text[i] == '\t') {
224 i++
225 }
226
227 v, err := strconv.ParseInt(string(text[i:]), 10, 64)
228 if err != nil {
229 return errors.Wrapf(err, "invalid comparison value")
230 }
231
232 *exp = ComparisonExpr{Op: op, Value: v}
233 return nil
234 }
235
236 func (pred *ModifiedLines) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) {
237 files, err := prctx.ChangedFiles()
238
239 predicateResult := common.PredicateResult{
240 ValuePhrase: "file modifications",
241 ConditionPhrase: "meet the modification conditions",
242 }
243
244 if err != nil {
245 return nil, errors.Wrap(err, "failed to list changed files")
246 }
247
248 var additions, deletions int64
249 for _, f := range files {
250 additions += int64(f.Additions)
251 deletions += int64(f.Deletions)
252 }
253
254 if !pred.Additions.IsEmpty() {
255 predicateResult.Values = []string{fmt.Sprintf("+%d", additions)}
256 predicateResult.ConditionValues = []string{fmt.Sprintf("added lines %s", pred.Additions.String())}
257 if pred.Additions.Evaluate(additions) {
258 predicateResult.Satisfied = true
259 return &predicateResult, nil
260 }
261 }
262 if !pred.Deletions.IsEmpty() {
263 if pred.Deletions.Evaluate(deletions) {
264 predicateResult.Values = []string{fmt.Sprintf("-%d", deletions)}
265 predicateResult.ConditionValues = []string{fmt.Sprintf("deleted lines %s", pred.Deletions.String())}
266 predicateResult.Satisfied = true
267 return &predicateResult, nil
268 }
269 predicateResult.Values = append(predicateResult.Values, fmt.Sprintf("-%d", deletions))
270 predicateResult.ConditionValues = append(predicateResult.ConditionValues, fmt.Sprintf("deleted lines %s", pred.Deletions.String()))
271 }
272 if !pred.Total.IsEmpty() {
273 if pred.Total.Evaluate(additions + deletions) {
274 predicateResult.Values = []string{fmt.Sprintf("total %d", additions+deletions)}
275 predicateResult.ConditionValues = []string{fmt.Sprintf("total modifications %s", pred.Total.String())}
276 predicateResult.Satisfied = true
277 return &predicateResult, nil
278 }
279 predicateResult.Values = append(predicateResult.Values, fmt.Sprintf("total %d", additions+deletions))
280 predicateResult.ConditionValues = append(predicateResult.ConditionValues, fmt.Sprintf("total modifications %s", pred.Total.String()))
281 }
282 predicateResult.Satisfied = false
283 return &predicateResult, nil
284 }
285
286 func (pred *ModifiedLines) Trigger() common.Trigger {
287 return common.TriggerCommit
288 }
289
290 var _ Predicate = &ModifiedLines{}
291
View as plain text