-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement resolving file paths to include
Supports glob patterns - "*" - all - "path/to/*" - files in dir (not recursive) - "path/to/f*" - files starting with f - "path/to/**" - recursive - "path/to/" - recursive Signed-off-by: AlexNg <[email protected]>
- Loading branch information
1 parent
bf6a2f2
commit 488298c
Showing
2 changed files
with
230 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,95 @@ | ||
package template | ||
|
||
import ( | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/caffeine-addictt/template/cmd/utils/types" | ||
) | ||
|
||
// Resolve paths to include | ||
// | ||
// Negation always takes priority. i.e. Set["path/to/file", "!path/**"] = Set[] | ||
// | ||
// Syntax: | ||
// | ||
// "*" is glob for everything | ||
// "path/to/file" is a single file | ||
// "path/to/f*" is a glob for a file | ||
// "path/to/dir/*" is a single dir level glob | ||
// "path/to/dir/**" == path/to/dir/ is a recursive dir level glob | ||
// "!path/to/file" is a negated ignore | ||
func ResolveIncludes(paths, ignores types.Set[string]) types.Set[string] { | ||
negation := types.NewSet[string]() | ||
|
||
// handle explicit includes | ||
for ignore := range ignores { | ||
if strings.HasPrefix(ignore, "!") { | ||
newIgnore := strings.TrimPrefix(ignore, "!") | ||
for _, p := range handleMatching(&paths, newIgnore) { | ||
negation.Add(p) | ||
} | ||
continue | ||
} | ||
} | ||
|
||
// handle "*" | ||
if ignores.Contains("*") { | ||
return negation | ||
} | ||
|
||
result := paths.Copy() | ||
for ignore := range ignores { | ||
// handle as removing | ||
for _, p := range handleMatching(&result, ignore) { | ||
result.Remove(p) | ||
} | ||
} | ||
|
||
return result.Union(negation) | ||
} | ||
|
||
func handleMatching(paths *types.Set[string], pattern string) []string { | ||
matching := make([]string, 0, paths.Len()/2) | ||
patternParts := strings.Split(pattern, "/") | ||
|
||
// convert pattern parts to regex-able | ||
nonRecursePartsCount := 0 | ||
isRecusive := false | ||
newPattern := "^" | ||
|
||
a: | ||
for nonRecursePartsCount < len(patternParts) { | ||
switch patternParts[nonRecursePartsCount] { | ||
case "**", "": | ||
isRecusive = true | ||
break a | ||
default: | ||
newPattern += strings.ReplaceAll(patternParts[nonRecursePartsCount], "*", ".*") + `/` | ||
} | ||
|
||
nonRecursePartsCount++ | ||
} | ||
|
||
newPattern = strings.TrimSuffix(newPattern, "/") + "$" | ||
re := regexp.MustCompile(newPattern) | ||
|
||
for p := range *paths { | ||
pParts := strings.Split(p, "/") | ||
|
||
if len(pParts) < nonRecursePartsCount { | ||
continue | ||
} | ||
|
||
common := strings.Join(pParts[:nonRecursePartsCount], "/") | ||
if !re.MatchString(common) { | ||
continue | ||
} | ||
|
||
if !isRecusive || (isRecusive && len(pParts) > nonRecursePartsCount) { | ||
matching = append(matching, p) | ||
} | ||
} | ||
|
||
return matching | ||
} |
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,135 @@ | ||
package template_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/caffeine-addictt/template/cmd/template" | ||
"github.com/caffeine-addictt/template/cmd/utils/types" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestResolveIncludes(t *testing.T) { | ||
tests := []struct { | ||
paths types.Set[string] | ||
ignores types.Set[string] | ||
expected types.Set[string] | ||
name string | ||
}{ | ||
{ | ||
paths: types.NewSet("path/to/file"), | ||
ignores: types.NewSet("path/to/file"), | ||
expected: types.NewSet[string](), | ||
name: "ignore single file", | ||
}, | ||
{ | ||
paths: types.NewSet("path/to/file"), | ||
ignores: types.NewSet("path/to/f*"), | ||
expected: types.NewSet[string](), | ||
name: "ignore single file glob", | ||
}, | ||
{ | ||
paths: types.NewSet("path/to/file"), | ||
ignores: types.NewSet("path/to/*"), | ||
expected: types.NewSet[string](), | ||
name: "ignore single dir level glob", | ||
}, | ||
{ | ||
paths: types.NewSet("path/to/file"), | ||
ignores: types.NewSet("path/**"), | ||
expected: types.NewSet[string](), | ||
name: "ignore recursive dir level glob", | ||
}, | ||
{ | ||
paths: types.NewSet("path/one", "path/two", "path/three"), | ||
ignores: types.NewSet("*"), | ||
expected: types.NewSet[string](), | ||
name: "ignore all files", | ||
}, | ||
{ | ||
paths: types.NewSet("path/to/one", "path/to/two", "path/to/three"), | ||
ignores: types.NewSet("path/to/*", "!path/to/one", "!path/to/two"), | ||
expected: types.NewSet("path/to/one", "path/to/two"), | ||
name: "include negated files", | ||
}, | ||
{ | ||
paths: types.NewSet("path/to/one"), | ||
ignores: types.NewSet("!path/to/o*", "path/to/one"), | ||
expected: types.NewSet("path/to/one"), | ||
name: "include negated file glob", | ||
}, | ||
{ | ||
paths: types.NewSet("path/to/one", "path/to/two", "path/to/three"), | ||
ignores: types.NewSet("!path/to/*", "path/**"), | ||
expected: types.NewSet("path/to/one", "path/to/two", "path/to/three"), | ||
name: "include negated dir level glob", | ||
}, | ||
{ | ||
paths: types.NewSet("path/to/one", "path/to/two", "path/to/three"), | ||
ignores: types.NewSet("!path/**", "path/to/*"), | ||
expected: types.NewSet("path/to/one", "path/to/two", "path/to/three"), | ||
name: "include negated recursive dir level glob", | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
result := template.ResolveIncludes(tc.paths, tc.ignores) | ||
assert.ElementsMatch(t, tc.expected.ToSlice(), result.ToSlice()) | ||
}) | ||
} | ||
} | ||
|
||
func BenchmarkResolveIncludes(b *testing.B) { | ||
for _, tc := range []struct { | ||
paths types.Set[string] | ||
ignores types.Set[string] | ||
name string | ||
}{ | ||
{ | ||
name: "Simple Exclude", | ||
paths: types.NewSet("path/to/file1", "path/to/file2", "path/to/dir/file3"), | ||
ignores: types.NewSet("path/to/file1"), | ||
}, | ||
{ | ||
name: "Single Level Glob Exclude", | ||
paths: types.NewSet("path/to/file1", "path/to/file2", "path/to/dir/file3"), | ||
ignores: types.NewSet("path/to/dir/*"), | ||
}, | ||
{ | ||
name: "Recursive Glob Exclude", | ||
paths: types.NewSet("path/to/file1", "path/to/file2", "path/to/dir/file3", "path/to/dir/subdir/file4"), | ||
ignores: types.NewSet("path/to/dir/**"), | ||
}, | ||
{ | ||
name: "Negation of Recursive Glob", | ||
paths: types.NewSet("path/to/file1", "path/to/file2", "path/to/dir/file3", "path/to/dir/subdir/file4"), | ||
ignores: types.NewSet("path/to/dir/**", "!path/to/dir/subdir/file4"), | ||
}, | ||
{ | ||
name: "Complex Pattern", | ||
paths: types.NewSet("path/to/file1", "path/to/file2", "path/to/dir/file3", "path/to/dir/subdir/file4", "path/to/otherfile"), | ||
ignores: types.NewSet("path/to/dir/**", "!path/to/dir/subdir/file4", "path/to/otherfile"), | ||
}, | ||
{ | ||
name: "Large Dataset", | ||
paths: generateLargeDataset(), | ||
ignores: types.NewSet("path/to/dir/**"), | ||
}, | ||
} { | ||
b.Run(tc.name, func(b *testing.B) { | ||
for i := 0; i < b.N; i++ { | ||
template.ResolveIncludes(tc.paths, tc.ignores) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// Helper function to generate a large dataset for benchmarking | ||
func generateLargeDataset() types.Set[string] { | ||
paths := make([]string, 0, 1000) | ||
for i := 0; i < 1000; i++ { | ||
paths = append(paths, fmt.Sprintf("path/to/dir/file%d", i)) | ||
} | ||
return types.NewSet(paths...) | ||
} |