Skip to content

Commit b25bae0

Browse files
committed
Fix use actions compatible globbing
1 parent d5fa2e7 commit b25bae0

File tree

4 files changed

+743
-35
lines changed

4 files changed

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

0 commit comments

Comments
 (0)