-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: workflowpattern package (#1618)
* feat: workflowpattern package * nolint:gocyclo --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
- Loading branch information
1 parent
4eba04b
commit 05eaeaa
Showing
3 changed files
with
628 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package workflowpattern | ||
|
||
import "fmt" | ||
|
||
type TraceWriter interface { | ||
Info(string, ...interface{}) | ||
} | ||
|
||
type EmptyTraceWriter struct{} | ||
|
||
func (*EmptyTraceWriter) Info(string, ...interface{}) { | ||
} | ||
|
||
type StdOutTraceWriter struct{} | ||
|
||
func (*StdOutTraceWriter) Info(format string, args ...interface{}) { | ||
fmt.Printf(format+"\n", args...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
package workflowpattern | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
type WorkflowPattern struct { | ||
Pattern string | ||
Negative bool | ||
Regex *regexp.Regexp | ||
} | ||
|
||
func CompilePattern(rawpattern string) (*WorkflowPattern, error) { | ||
negative := false | ||
pattern := rawpattern | ||
if strings.HasPrefix(rawpattern, "!") { | ||
negative = true | ||
pattern = rawpattern[1:] | ||
} | ||
rpattern, err := PatternToRegex(pattern) | ||
if err != nil { | ||
return nil, err | ||
} | ||
regex, err := regexp.Compile(rpattern) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &WorkflowPattern{ | ||
Pattern: pattern, | ||
Negative: negative, | ||
Regex: regex, | ||
}, nil | ||
} | ||
|
||
//nolint:gocyclo | ||
func PatternToRegex(pattern string) (string, error) { | ||
var rpattern strings.Builder | ||
rpattern.WriteString("^") | ||
pos := 0 | ||
errors := map[int]string{} | ||
for pos < len(pattern) { | ||
switch pattern[pos] { | ||
case '*': | ||
if pos+1 < len(pattern) && pattern[pos+1] == '*' { | ||
if pos+2 < len(pattern) && pattern[pos+2] == '/' { | ||
rpattern.WriteString("(.+/)?") | ||
pos += 3 | ||
} else { | ||
rpattern.WriteString(".*") | ||
pos += 2 | ||
} | ||
} else { | ||
rpattern.WriteString("[^/]*") | ||
pos++ | ||
} | ||
case '+', '?': | ||
if pos > 0 { | ||
rpattern.WriteByte(pattern[pos]) | ||
} else { | ||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) | ||
} | ||
pos++ | ||
case '[': | ||
rpattern.WriteByte(pattern[pos]) | ||
pos++ | ||
if pos < len(pattern) && pattern[pos] == ']' { | ||
errors[pos] = "Unexpected empty brackets '[]'" | ||
pos++ | ||
break | ||
} | ||
validChar := func(a, b, test byte) bool { | ||
return test >= a && test <= b | ||
} | ||
startPos := pos | ||
for pos < len(pattern) && pattern[pos] != ']' { | ||
switch pattern[pos] { | ||
case '-': | ||
if pos <= startPos || pos+1 >= len(pattern) { | ||
errors[pos] = "Invalid range" | ||
pos++ | ||
break | ||
} | ||
validRange := func(a, b byte) bool { | ||
return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1] | ||
} | ||
if !validRange('A', 'z') && !validRange('0', '9') { | ||
errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9" | ||
pos++ | ||
break | ||
} | ||
rpattern.WriteString(pattern[pos : pos+2]) | ||
pos += 2 | ||
default: | ||
if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) { | ||
errors[pos] = "Ranges can only include a-z, A-Z and 0-9" | ||
pos++ | ||
break | ||
} | ||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) | ||
pos++ | ||
} | ||
} | ||
if pos >= len(pattern) || pattern[pos] != ']' { | ||
errors[pos] = "Missing closing bracket ']' after '['" | ||
pos++ | ||
} | ||
rpattern.WriteString("]") | ||
pos++ | ||
case '\\': | ||
if pos+1 >= len(pattern) { | ||
errors[pos] = "Missing symbol after \\" | ||
pos++ | ||
break | ||
} | ||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]}))) | ||
pos += 2 | ||
default: | ||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) | ||
pos++ | ||
} | ||
} | ||
if len(errors) > 0 { | ||
var errorMessage strings.Builder | ||
for position, err := range errors { | ||
if errorMessage.Len() > 0 { | ||
errorMessage.WriteString(", ") | ||
} | ||
errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err)) | ||
} | ||
return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String()) | ||
} | ||
rpattern.WriteString("$") | ||
return rpattern.String(), nil | ||
} | ||
|
||
func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { | ||
ret := []*WorkflowPattern{} | ||
for _, pattern := range patterns { | ||
cp, err := CompilePattern(pattern) | ||
if err != nil { | ||
return nil, err | ||
} | ||
ret = append(ret, cp) | ||
} | ||
return ret, nil | ||
} | ||
|
||
// returns true if the workflow should be skipped paths/branches | ||
func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { | ||
if len(sequence) == 0 { | ||
return false | ||
} | ||
for _, file := range input { | ||
matched := false | ||
for _, item := range sequence { | ||
if item.Regex.MatchString(file) { | ||
pattern := item.Pattern | ||
if item.Negative { | ||
matched = false | ||
traceWriter.Info("%s excluded by pattern %s", file, pattern) | ||
} else { | ||
matched = true | ||
traceWriter.Info("%s included by pattern %s", file, pattern) | ||
} | ||
} | ||
} | ||
if matched { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
// returns true if the workflow should be skipped paths-ignore/branches-ignore | ||
func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { | ||
if len(sequence) == 0 { | ||
return false | ||
} | ||
for _, file := range input { | ||
matched := false | ||
for _, item := range sequence { | ||
if item.Regex.MatchString(file) == !item.Negative { | ||
pattern := item.Pattern | ||
traceWriter.Info("%s ignored by pattern %s", file, pattern) | ||
matched = true | ||
break | ||
} | ||
} | ||
if !matched { | ||
return false | ||
} | ||
} | ||
return true | ||
} |
Oops, something went wrong.