mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-16 01:43:24 +09:00
refactor: move workflowpattern into modules/actions (#37717)
`act/workflowpattern` in runner is only consumed by Gitea and dead code there. Move it to this repo. Use `modules/glob` for glob pattern match. --------- Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
co-authored by
Claude
wxiaoguang
parent
02be228ed6
commit
16189a68c4
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflowpattern
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/glob"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkflowPattern struct {
|
||||||
|
negative bool
|
||||||
|
glob glob.Glob
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) {
|
||||||
|
ret := make([]*WorkflowPattern, 0, len(patterns))
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
cp, err := glob.CompileWorkflow(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret = append(ret, &WorkflowPattern{glob: cp, negative: strings.HasPrefix(pattern, "!")})
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip returns true if the workflow should be skipped per paths/branches semantics.
|
||||||
|
func Skip(sequence []*WorkflowPattern, input []string) bool {
|
||||||
|
allSkipped := true
|
||||||
|
for _, file := range input {
|
||||||
|
shouldSkip := true
|
||||||
|
for _, item := range sequence {
|
||||||
|
if item.negative {
|
||||||
|
// "!README.md" doesn't match "README.md", so "README.md" should be skipped
|
||||||
|
// "!README.md" matches "help.md" but it shouldn't affect "skip or not", because "help.md" might have been skipped by other rules like "docs/*.md"
|
||||||
|
if !item.glob.Match(file) {
|
||||||
|
shouldSkip = true
|
||||||
|
}
|
||||||
|
} else if item.glob.Match(file) {
|
||||||
|
// if "*.md" matches "help.md" so it shouldn't be skipped
|
||||||
|
shouldSkip = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allSkipped = allSkipped && shouldSkip
|
||||||
|
}
|
||||||
|
return len(sequence) > 0 && allSkipped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter returns true if the workflow should be skipped per paths-ignore/branches-ignore semantics.
|
||||||
|
func Filter(sequence []*WorkflowPattern, input []string) bool {
|
||||||
|
for _, file := range input {
|
||||||
|
anyMatched := false
|
||||||
|
for _, item := range sequence {
|
||||||
|
if anyMatched = item.glob.Match(file); anyMatched {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !anyMatched {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(sequence) != 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflowpattern
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatchPattern(t *testing.T) {
|
||||||
|
kases := []struct {
|
||||||
|
inputs []string
|
||||||
|
patterns []string
|
||||||
|
skipResult bool
|
||||||
|
filterResult bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
patterns: []string{"*"},
|
||||||
|
inputs: []string{"path/with/slash"},
|
||||||
|
skipResult: true,
|
||||||
|
filterResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"path/a", "path/b", "path/c"},
|
||||||
|
inputs: []string{"meta", "path/b", "otherfile"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"path/a", "path/b", "path/c"},
|
||||||
|
inputs: []string{"path/b"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"path/a", "path/b", "path/c"},
|
||||||
|
inputs: []string{"path/c", "path/b"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"path/a", "path/b", "path/c"},
|
||||||
|
inputs: []string{"path/c", "path/b", "path/a"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"path/a", "path/b", "path/c"},
|
||||||
|
inputs: []string{"path/c", "path/b", "path/d", "path/a"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{},
|
||||||
|
inputs: []string{},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"\\!file"},
|
||||||
|
inputs: []string{"!file"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"escape\\\\backslash"},
|
||||||
|
inputs: []string{"escape\\backslash"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{".yml"},
|
||||||
|
inputs: []string{"fyml"},
|
||||||
|
skipResult: true,
|
||||||
|
filterResult: false,
|
||||||
|
},
|
||||||
|
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags
|
||||||
|
{
|
||||||
|
patterns: []string{"feature/*"},
|
||||||
|
inputs: []string{"feature/my-branch"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"feature/*"},
|
||||||
|
inputs: []string{"feature/your-branch"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"feature/**"},
|
||||||
|
inputs: []string{"feature/beta-a/my-branch"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"feature/**"},
|
||||||
|
inputs: []string{"feature/beta-a/my-branch"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"feature/**"},
|
||||||
|
inputs: []string{"feature/mona/the/octocat"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"main", "releases/mona-the-octocat"},
|
||||||
|
inputs: []string{"main"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"main", "releases/mona-the-octocat"},
|
||||||
|
inputs: []string{"releases/mona-the-octocat"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*"},
|
||||||
|
inputs: []string{"main"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*"},
|
||||||
|
inputs: []string{"releases"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**"},
|
||||||
|
inputs: []string{"all/the/branches"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**"},
|
||||||
|
inputs: []string{"every/tag"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*feature"},
|
||||||
|
inputs: []string{"mona-feature"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*feature"},
|
||||||
|
inputs: []string{"feature"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*feature"},
|
||||||
|
inputs: []string{"ver-10-feature"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"v2*"},
|
||||||
|
inputs: []string{"v2"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"v2*"},
|
||||||
|
inputs: []string{"v2.0"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"v2*"},
|
||||||
|
inputs: []string{"v2.9"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
||||||
|
inputs: []string{"v1.10.1"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
||||||
|
inputs: []string{"v2.0.0"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths
|
||||||
|
{
|
||||||
|
patterns: []string{"*"},
|
||||||
|
inputs: []string{"README.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*"},
|
||||||
|
inputs: []string{"server.rb"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.jsx?"},
|
||||||
|
inputs: []string{"page.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.jsx?"},
|
||||||
|
inputs: []string{"page.jsx"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**"},
|
||||||
|
inputs: []string{"all/the/files.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.js"},
|
||||||
|
inputs: []string{"app.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.js"},
|
||||||
|
inputs: []string{"index.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**.js"},
|
||||||
|
inputs: []string{"index.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**.js"},
|
||||||
|
inputs: []string{"js/index.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**.js"},
|
||||||
|
inputs: []string{"src/js/app.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"docs/*"},
|
||||||
|
inputs: []string{"docs/README.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"docs/*"},
|
||||||
|
inputs: []string{"docs/file.txt"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"docs/**"},
|
||||||
|
inputs: []string{"docs/README.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"docs/**"},
|
||||||
|
inputs: []string{"docs/mona/octocat.txt"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"docs/**/*.md"},
|
||||||
|
inputs: []string{"docs/README.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"docs/**/*.md"},
|
||||||
|
inputs: []string{"docs/mona/hello-world.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"docs/**/*.md"},
|
||||||
|
inputs: []string{"docs/a/markdown/file.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/docs/**"},
|
||||||
|
inputs: []string{"docs/hello.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/docs/**"},
|
||||||
|
inputs: []string{"dir/docs/my-file.txt"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/docs/**"},
|
||||||
|
inputs: []string{"space/docs/plan/space.doc"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/README.md"},
|
||||||
|
inputs: []string{"README.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/README.md"},
|
||||||
|
inputs: []string{"js/README.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/*src/**"},
|
||||||
|
inputs: []string{"a/src/app.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/*src/**"},
|
||||||
|
inputs: []string{"my-src/code/js/app.js"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/*-post.md"},
|
||||||
|
inputs: []string{"my-post.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/*-post.md"},
|
||||||
|
inputs: []string{"path/their-post.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/migrate-*.sql"},
|
||||||
|
inputs: []string{"migrate-10909.sql"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/migrate-*.sql"},
|
||||||
|
inputs: []string{"db/migrate-v1.0.sql"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"**/migrate-*.sql"},
|
||||||
|
inputs: []string{"db/sept/migrate-v1.sql"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.md", "!README.md"},
|
||||||
|
inputs: []string{"hello.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.md", "!README.md"},
|
||||||
|
inputs: []string{"README.md"},
|
||||||
|
skipResult: true,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.md", "!README.md"},
|
||||||
|
inputs: []string{"docs/hello.md"},
|
||||||
|
skipResult: true,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.md", "!README.md", "README*"},
|
||||||
|
inputs: []string{"hello.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.md", "!README.md", "README*"},
|
||||||
|
inputs: []string{"README.md"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []string{"*.md", "!README.md", "README*"},
|
||||||
|
inputs: []string{"README.doc"},
|
||||||
|
skipResult: false,
|
||||||
|
filterResult: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kase := range kases {
|
||||||
|
msg := fmt.Sprintf("patterns=%s, input=%s", strings.Join(kase.patterns, ","), strings.Join(kase.inputs, ","))
|
||||||
|
patterns, err := CompilePatterns(kase.patterns...)
|
||||||
|
assert.NoError(t, err, "compile error: %s", msg)
|
||||||
|
assert.Equal(t, kase.skipResult, Skip(patterns, kase.inputs), "unexpected skip result: %s", msg)
|
||||||
|
assert.Equal(t, kase.filterResult, Filter(patterns, kase.inputs), "unexpected filter result: %s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/actions/jobparser"
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
|
"code.gitea.io/gitea/modules/actions/workflowpattern"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/glob"
|
"code.gitea.io/gitea/modules/glob"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@@ -18,7 +19,6 @@ import (
|
|||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
"gitea.com/gitea/runner/act/workflowpattern"
|
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Skip(patterns, []string{refName.BranchName()}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Skip(patterns, []string{refName.BranchName()}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "branches-ignore":
|
case "branches-ignore":
|
||||||
@@ -309,7 +309,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Filter(patterns, []string{refName.BranchName()}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Filter(patterns, []string{refName.BranchName()}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "tags":
|
case "tags":
|
||||||
@@ -321,7 +321,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Skip(patterns, []string{refName.TagName()}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Skip(patterns, []string{refName.TagName()}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "tags-ignore":
|
case "tags-ignore":
|
||||||
@@ -333,7 +333,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Filter(patterns, []string{refName.TagName()}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Filter(patterns, []string{refName.TagName()}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "paths":
|
case "paths":
|
||||||
@@ -349,7 +349,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +366,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,7 +492,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Skip(patterns, []string{refName.ShortName()}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "branches-ignore":
|
case "branches-ignore":
|
||||||
@@ -501,7 +501,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Filter(patterns, []string{refName.ShortName()}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "paths":
|
case "paths":
|
||||||
@@ -513,7 +513,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Skip(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,7 +526,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Filter(patterns, filesChanged) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -747,7 +747,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Skip(patterns, []string{workflow.Name}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "branches":
|
case "branches":
|
||||||
@@ -755,7 +755,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "branches-ignore":
|
case "branches-ignore":
|
||||||
@@ -763,7 +763,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
|
if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}) {
|
||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
+60
-7
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
@@ -18,11 +19,18 @@ type Glob interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type globCompiler struct {
|
type globCompiler struct {
|
||||||
|
regexpQuestion bool
|
||||||
|
regexpPlus bool
|
||||||
|
superWildcardRight bool
|
||||||
|
supportNegative bool
|
||||||
|
|
||||||
|
separators []rune
|
||||||
nonSeparatorChars string
|
nonSeparatorChars string
|
||||||
globPattern []rune
|
globPattern []rune
|
||||||
regexpPattern string
|
regexpPattern string
|
||||||
regexp *regexp.Regexp
|
regexp *regexp.Regexp
|
||||||
pos int
|
pos int
|
||||||
|
negativeFlip bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileChars compiles character class patterns like [abc] or [!abc]
|
// compileChars compiles character class patterns like [abc] or [!abc]
|
||||||
@@ -70,13 +78,39 @@ func (g *globCompiler) compile(subPattern bool) (string, error) {
|
|||||||
switch c {
|
switch c {
|
||||||
case '*':
|
case '*':
|
||||||
if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '*' {
|
if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '*' {
|
||||||
g.pos++
|
var matchRightSep bool
|
||||||
|
if g.superWildcardRight {
|
||||||
|
// check "**/" pattern, then the wildcards should also match the right separator
|
||||||
|
// e.g.: "**/docs" should match "docs"
|
||||||
|
var rightRune rune
|
||||||
|
if g.pos+1 < len(g.globPattern) {
|
||||||
|
rightRune = g.globPattern[g.pos+1]
|
||||||
|
}
|
||||||
|
if slices.Contains(g.separators, rightRune) {
|
||||||
|
matchRightSep = g.pos-2 < 0 || g.globPattern[g.pos-2] == rightRune
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matchRightSep {
|
||||||
|
g.pos += 2
|
||||||
|
} else {
|
||||||
|
g.pos++
|
||||||
|
}
|
||||||
result += ".*" // match any sequence of characters
|
result += ".*" // match any sequence of characters
|
||||||
} else {
|
} else {
|
||||||
result += g.nonSeparatorChars + "*" // match any sequence of non-separator characters
|
result += g.nonSeparatorChars + "*" // match any sequence of non-separator characters
|
||||||
}
|
}
|
||||||
case '?':
|
case '?':
|
||||||
result += g.nonSeparatorChars // match any single non-separator character
|
if g.regexpQuestion {
|
||||||
|
result += "?"
|
||||||
|
} else {
|
||||||
|
result += g.nonSeparatorChars // match any single non-separator character
|
||||||
|
}
|
||||||
|
case '+':
|
||||||
|
if g.regexpPlus {
|
||||||
|
result += "+"
|
||||||
|
} else {
|
||||||
|
result += "\\" + string(c)
|
||||||
|
}
|
||||||
case '[':
|
case '[':
|
||||||
chars, err := g.compileChars()
|
chars, err := g.compileChars()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,7 +135,7 @@ func (g *globCompiler) compile(subPattern bool) (string, error) {
|
|||||||
}
|
}
|
||||||
result += "\\" + string(g.globPattern[g.pos])
|
result += "\\" + string(g.globPattern[g.pos])
|
||||||
g.pos++
|
g.pos++
|
||||||
case '.', '+', '^', '$', '(', ')', '|':
|
case '.', '^', '$', '(', ')', '|':
|
||||||
result += "\\" + string(c) // escape regexp special characters
|
result += "\\" + string(c) // escape regexp special characters
|
||||||
default:
|
default:
|
||||||
result += string(c)
|
result += string(c)
|
||||||
@@ -111,8 +145,9 @@ func (g *globCompiler) compile(subPattern bool) (string, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGlobCompiler(pattern string, separators ...rune) (Glob, error) {
|
func initGlobCompiler(g *globCompiler, pattern string, separators []rune) (Glob, error) {
|
||||||
g := &globCompiler{globPattern: []rune(pattern)}
|
g.globPattern = []rune(pattern)
|
||||||
|
g.separators = separators
|
||||||
|
|
||||||
// Escape separators for use in character class
|
// Escape separators for use in character class
|
||||||
escapedSeparators := regexp.QuoteMeta(string(separators))
|
escapedSeparators := regexp.QuoteMeta(string(separators))
|
||||||
@@ -122,6 +157,11 @@ func newGlobCompiler(pattern string, separators ...rune) (Glob, error) {
|
|||||||
g.nonSeparatorChars = "."
|
g.nonSeparatorChars = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if g.supportNegative && len(g.globPattern) > 0 && g.globPattern[0] == '!' {
|
||||||
|
g.negativeFlip = true
|
||||||
|
g.pos++
|
||||||
|
}
|
||||||
|
|
||||||
compiled, err := g.compile(false)
|
compiled, err := g.compile(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -139,11 +179,24 @@ func newGlobCompiler(pattern string, separators ...rune) (Glob, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *globCompiler) Match(s string) bool {
|
func (g *globCompiler) Match(s string) bool {
|
||||||
return g.regexp.MatchString(s)
|
ret := g.regexp.MatchString(s)
|
||||||
|
if g.negativeFlip {
|
||||||
|
ret = !ret
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func Compile(pattern string, separators ...rune) (Glob, error) {
|
func Compile(pattern string, separators ...rune) (Glob, error) {
|
||||||
return newGlobCompiler(pattern, separators...)
|
return initGlobCompiler(&globCompiler{}, pattern, separators)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompileWorkflow(pattern string) (Glob, error) {
|
||||||
|
return initGlobCompiler(&globCompiler{
|
||||||
|
regexpQuestion: true,
|
||||||
|
regexpPlus: true,
|
||||||
|
superWildcardRight: true,
|
||||||
|
supportNegative: true,
|
||||||
|
}, pattern, []rune{'/'})
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustCompile(pattern string, separators ...rune) Glob {
|
func MustCompile(pattern string, separators ...rune) Glob {
|
||||||
|
|||||||
Reference in New Issue
Block a user