Skip to content

Commit

Permalink
feat: Implement resolving file paths to include
Browse files Browse the repository at this point in the history
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
caffeine-addictt committed Sep 2, 2024
1 parent bf6a2f2 commit 488298c
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
95 changes: 95 additions & 0 deletions cmd/template/ignore.go
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
}
135 changes: 135 additions & 0 deletions cmd/template/ignore_test.go
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...)
}

0 comments on commit 488298c

Please sign in to comment.