Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions internal/config/triggers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package config

import (
"path/filepath"
"path"
"strings"
)

Expand Down Expand Up @@ -81,18 +81,18 @@ func MatchAnyTrigger(patterns []string, changedFiles []string) bool {
// level by MatchTrigger. Supports "*" (single segment), "**" (any number of
// segments) and "?" (single character), matching the grammar used by the
// emitted GitHub Actions paths filter.
func MatchGlobPattern(pattern, path string) bool {
return matchGlobPattern(stripNegation(pattern), path)
func MatchGlobPattern(pattern, filePath string) bool {
return matchGlobPattern(stripNegation(pattern), filePath)
}

// matchGlobPattern matches a file path against a single (already
// negation-stripped) glob pattern.
func matchGlobPattern(pattern, path string) bool {
func matchGlobPattern(pattern, filePath string) bool {
if strings.Contains(pattern, "**") {
return matchDoublestarPattern(pattern, path)
return matchDoublestarPattern(pattern, filePath)
}

matched, _ := filepath.Match(pattern, path)
matched, _ := path.Match(pattern, filePath)
return matched
}

Expand All @@ -103,26 +103,26 @@ func matchDoublestarPattern(pattern, path string) bool {
return matchGlobParts(patternParts, pathParts)
}

func matchGlobParts(pattern, path []string) bool {
func matchGlobParts(pattern, pathParts []string) bool {
pi, ppi := 0, 0

for pi < len(pattern) && ppi < len(path) {
for pi < len(pattern) && ppi < len(pathParts) {
if pattern[pi] == "**" {
if pi == len(pattern)-1 {
// "**" at the end matches everything remaining.
return true
}

// Try matching the remaining pattern at each subsequent position.
for i := ppi; i <= len(path); i++ {
if matchGlobParts(pattern[pi+1:], path[i:]) {
for i := ppi; i <= len(pathParts); i++ {
if matchGlobParts(pattern[pi+1:], pathParts[i:]) {
return true
}
}
return false
}

matched, _ := filepath.Match(pattern[pi], path[ppi])
matched, _ := path.Match(pattern[pi], pathParts[ppi])
if !matched {
return false
}
Expand All @@ -139,5 +139,5 @@ func matchGlobParts(pattern, path []string) bool {
pi++
}

return ppi == len(path)
return ppi == len(pathParts)
}
32 changes: 32 additions & 0 deletions internal/config/triggers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,38 @@ func TestIsNegationPattern(t *testing.T) {
}
}

// TestMatchGlobPattern_SlashBoundary locks in the slash-native matching
// semantics that path.Match provides: a single "*" matches within one path
// segment and never crosses a "/", matching the GitHub Actions paths filter
// grammar this evaluator is meant to mirror. These cases pass on Linux/macOS
// under either path.Match or filepath.Match (the separator is "/" on both),
// so they are guard/documentation tests that would catch a future regression
// to a matcher whose separator is platform-dependent; the Windows behavioral
// delta (filepath.Match treating "/" as an ordinary character) cannot be
// exercised on Linux CI.
func TestMatchGlobPattern_SlashBoundary(t *testing.T) {
tests := []struct {
name string
pattern string
file string
want bool
}{
{"single star does not cross slash", "a/*/b", "a/x/y/b", false},
{"single star matches one segment", "a/*/b", "a/x/b", true},
{"dir star does not descend", "dir/*", "dir/sub/file", false},
{"dir star matches direct child", "dir/*", "dir/file", true},
{"doublestar still matches nested files", "**/*.go", "a/b/c.go", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := MatchGlobPattern(tt.pattern, tt.file); got != tt.want {
t.Errorf("MatchGlobPattern(%q, %q) = %v, want %v", tt.pattern, tt.file, got, tt.want)
}
})
}
}

func TestMatchGlobPattern_StripsNegation(t *testing.T) {
// The bare-glob helper matches whether the glob (negation stripped) matches.
if !MatchGlobPattern("!**/*.md", "docs/README.md") {
Expand Down