Skip to content

Commit 05eaeaa

Browse files
feat: workflowpattern package (#1618)
* feat: workflowpattern package * nolint:gocyclo --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 4eba04b commit 05eaeaa

File tree

3 files changed

+628
-0
lines changed

3 files changed

+628
-0
lines changed

pkg/workflowpattern/trace_writer.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package workflowpattern
2+
3+
import "fmt"
4+
5+
type TraceWriter interface {
6+
Info(string, ...interface{})
7+
}
8+
9+
type EmptyTraceWriter struct{}
10+
11+
func (*EmptyTraceWriter) Info(string, ...interface{}) {
12+
}
13+
14+
type StdOutTraceWriter struct{}
15+
16+
func (*StdOutTraceWriter) Info(format string, args ...interface{}) {
17+
fmt.Printf(format+"\n", args...)
18+
}
+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package workflowpattern
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
type WorkflowPattern struct {
10+
Pattern string
11+
Negative bool
12+
Regex *regexp.Regexp
13+
}
14+
15+
func CompilePattern(rawpattern string) (*WorkflowPattern, error) {
16+
negative := false
17+
pattern := rawpattern
18+
if strings.HasPrefix(rawpattern, "!") {
19+
negative = true
20+
pattern = rawpattern[1:]
21+
}
22+
rpattern, err := PatternToRegex(pattern)
23+
if err != nil {
24+
return nil, err
25+
}
26+
regex, err := regexp.Compile(rpattern)
27+
if err != nil {
28+
return nil, err
29+
}
30+
return &WorkflowPattern{
31+
Pattern: pattern,
32+
Negative: negative,
33+
Regex: regex,
34+
}, nil
35+
}
36+
37+
//nolint:gocyclo
38+
func PatternToRegex(pattern string) (string, error) {
39+
var rpattern strings.Builder
40+
rpattern.WriteString("^")
41+
pos := 0
42+
errors := map[int]string{}
43+
for pos < len(pattern) {
44+
switch pattern[pos] {
45+
case '*':
46+
if pos+1 < len(pattern) && pattern[pos+1] == '*' {
47+
if pos+2 < len(pattern) && pattern[pos+2] == '/' {
48+
rpattern.WriteString("(.+/)?")
49+
pos += 3
50+
} else {
51+
rpattern.WriteString(".*")
52+
pos += 2
53+
}
54+
} else {
55+
rpattern.WriteString("[^/]*")
56+
pos++
57+
}
58+
case '+', '?':
59+
if pos > 0 {
60+
rpattern.WriteByte(pattern[pos])
61+
} else {
62+
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
63+
}
64+
pos++
65+
case '[':
66+
rpattern.WriteByte(pattern[pos])
67+
pos++
68+
if pos < len(pattern) && pattern[pos] == ']' {
69+
errors[pos] = "Unexpected empty brackets '[]'"
70+
pos++
71+
break
72+
}
73+
validChar := func(a, b, test byte) bool {
74+
return test >= a && test <= b
75+
}
76+
startPos := pos
77+
for pos < len(pattern) && pattern[pos] != ']' {
78+
switch pattern[pos] {
79+
case '-':
80+
if pos <= startPos || pos+1 >= len(pattern) {
81+
errors[pos] = "Invalid range"
82+
pos++
83+
break
84+
}
85+
validRange := func(a, b byte) bool {
86+
return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1]
87+
}
88+
if !validRange('A', 'z') && !validRange('0', '9') {
89+
errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9"
90+
pos++
91+
break
92+
}
93+
rpattern.WriteString(pattern[pos : pos+2])
94+
pos += 2
95+
default:
96+
if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) {
97+
errors[pos] = "Ranges can only include a-z, A-Z and 0-9"
98+
pos++
99+
break
100+
}
101+
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
102+
pos++
103+
}
104+
}
105+
if pos >= len(pattern) || pattern[pos] != ']' {
106+
errors[pos] = "Missing closing bracket ']' after '['"
107+
pos++
108+
}
109+
rpattern.WriteString("]")
110+
pos++
111+
case '\\':
112+
if pos+1 >= len(pattern) {
113+
errors[pos] = "Missing symbol after \\"
114+
pos++
115+
break
116+
}
117+
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]})))
118+
pos += 2
119+
default:
120+
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
121+
pos++
122+
}
123+
}
124+
if len(errors) > 0 {
125+
var errorMessage strings.Builder
126+
for position, err := range errors {
127+
if errorMessage.Len() > 0 {
128+
errorMessage.WriteString(", ")
129+
}
130+
errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err))
131+
}
132+
return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String())
133+
}
134+
rpattern.WriteString("$")
135+
return rpattern.String(), nil
136+
}
137+
138+
func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) {
139+
ret := []*WorkflowPattern{}
140+
for _, pattern := range patterns {
141+
cp, err := CompilePattern(pattern)
142+
if err != nil {
143+
return nil, err
144+
}
145+
ret = append(ret, cp)
146+
}
147+
return ret, nil
148+
}
149+
150+
// returns true if the workflow should be skipped paths/branches
151+
func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
152+
if len(sequence) == 0 {
153+
return false
154+
}
155+
for _, file := range input {
156+
matched := false
157+
for _, item := range sequence {
158+
if item.Regex.MatchString(file) {
159+
pattern := item.Pattern
160+
if item.Negative {
161+
matched = false
162+
traceWriter.Info("%s excluded by pattern %s", file, pattern)
163+
} else {
164+
matched = true
165+
traceWriter.Info("%s included by pattern %s", file, pattern)
166+
}
167+
}
168+
}
169+
if matched {
170+
return false
171+
}
172+
}
173+
return true
174+
}
175+
176+
// returns true if the workflow should be skipped paths-ignore/branches-ignore
177+
func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
178+
if len(sequence) == 0 {
179+
return false
180+
}
181+
for _, file := range input {
182+
matched := false
183+
for _, item := range sequence {
184+
if item.Regex.MatchString(file) == !item.Negative {
185+
pattern := item.Pattern
186+
traceWriter.Info("%s ignored by pattern %s", file, pattern)
187+
matched = true
188+
break
189+
}
190+
}
191+
if !matched {
192+
return false
193+
}
194+
}
195+
return true
196+
}

0 commit comments

Comments
 (0)