diff --git a/cmd/cloud.go b/cmd/cloud.go index 722d4417..04dc5610 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -148,6 +148,7 @@ func createProjectIgnoreRules(dir string, theproject *project.Project) *ignore.R errsystem.WithContextMessage(fmt.Sprintf("Error adding project ignore rule: %s. %s", rule, err))).ShowErrorAndExit() } } + return rules } diff --git a/internal/ignore/rules.go b/internal/ignore/rules.go index 02427292..cc03a918 100644 --- a/internal/ignore/rules.go +++ b/internal/ignore/rules.go @@ -52,6 +52,14 @@ func Empty() *Rules { return &Rules{patterns: []*pattern{}} } +func (r *Rules) String() string { + buf := bytes.NewBufferString("") + for _, p := range r.patterns { + buf.WriteString(p.raw + "\n") + } + return buf.String() +} + // AddDefaults adds default ignore patterns. func (r *Rules) AddDefaults() { r.parseRule("**/.venv/**/*") @@ -140,20 +148,42 @@ func (r *Rules) Ignore(path string, fi os.FileInfo) bool { if path == "." || path == "./" { return false } - for _, p := range r.patterns { + + var fullWildcard bool + + for n, p := range r.patterns { if p.match == nil { log.Printf("ignore: no matcher supplied for %q", p.raw) return false } + // this is a special case for the first rule, which is a full wildcard + // and this means the following rules are all negated and should only + // only return files that match + if n == 0 && p.fullWildcard { + fullWildcard = true + continue + } + // For negative rules, we need to capture and return non-matches, // and continue for matches. if p.negate { - if p.mustDir && !fi.IsDir() { - return true - } - if !p.match(path, fi) { - return true + // if full wildcard, we inverse the negation to only match files that match the following rules + if fullWildcard { + if p.mustDir && fi.IsDir() { + return false + } + if p.match(path, fi) { + return false + } + } else { + // otherwise, we only match files that don't match the rule + if p.mustDir && !fi.IsDir() { + return true + } + if !p.match(path, fi) { + return true + } } continue } @@ -167,7 +197,7 @@ func (r *Rules) Ignore(path string, fi os.FileInfo) bool { return true } } - return false + return fullWildcard } // parseRule parses a rule string and creates a pattern, which is then stored in the Rules object. @@ -183,8 +213,22 @@ func (r *Rules) parseRule(rule string) error { return nil } - // Special case for agentuity build folder - if rule == ".agentuity" || rule == ".agentuity/**" { + // this is a special case rule where we're saying we want to ignore everything + // and then use negate rules to only include files that match the rule + if rule == "**/*" { + p := &pattern{raw: rule, fullWildcard: true} + p.match = func(n string, fi os.FileInfo) bool { + return true + } + newpatterns := make([]*pattern, 0) + // filter out any rules that aren't negated in case they come before + // the full wildcard rule + for _, pattern := range r.patterns { + if !pattern.fullWildcard && pattern.negate { + newpatterns = append(newpatterns, pattern) + } + } + r.patterns = append([]*pattern{p}, newpatterns...) return nil } @@ -250,6 +294,10 @@ func (r *Rules) parseRule(rule string) error { } } + if len(r.patterns) > 0 && r.patterns[0].fullWildcard && !p.negate { + return nil // skip adding the rule if it's a full wildcard and not a negation + } + r.patterns = append(r.patterns, p) return nil } @@ -268,5 +316,6 @@ type pattern struct { // negate indicates that the rule's outcome should be negated. negate bool // mustDir indicates that the matched file must be a directory. - mustDir bool + mustDir bool + fullWildcard bool } diff --git a/internal/ignore/rules_test.go b/internal/ignore/rules_test.go index e432d7fa..e0595d12 100644 --- a/internal/ignore/rules_test.go +++ b/internal/ignore/rules_test.go @@ -37,3 +37,57 @@ func TestRules(t *testing.T) { assert.True(t, rules.Ignore("/Users/foobar/example/src/__test__/test_bar.py", nil)) assert.True(t, rules.Ignore("/Users/foobar/example/.agentuity-12345", nil)) } + +func TestNegateRules(t *testing.T) { + rules := Empty() + rules.AddDefaults() + rules.Add("!**/foo.py") + assert.False(t, rules.Ignore("/Users/foobar/example/src/foo.py", nil)) + assert.False(t, rules.Ignore("foo.py", nil)) + assert.True(t, rules.Ignore("bar.py", nil)) +} + +func TestFullWildcardRules(t *testing.T) { + rules := Empty() + rules.AddDefaults() + rules.Add("**/*") + rules.Add("!.agentuity/**") + rules.Add("!agentuity.yaml") + assert.False(t, rules.Ignore(".agentuity/foo.py", nil)) + assert.False(t, rules.Ignore("agentuity.yaml", nil)) + assert.True(t, rules.Ignore("bar.py", nil)) +} + +func TestFullWildcardRulesAfter(t *testing.T) { + rules := Empty() + rules.AddDefaults() + rules.Add("!.agentuity/**") + rules.Add("!agentuity.yaml") + rules.Add("**/*") + assert.False(t, rules.Ignore(".agentuity/foo.py", nil)) + assert.False(t, rules.Ignore("agentuity.yaml", nil)) + assert.True(t, rules.Ignore("bar.py", nil)) +} + +func TestFullWildcardRulesBetween(t *testing.T) { + rules := Empty() + rules.AddDefaults() + rules.Add("!.agentuity/**") + rules.Add("**/*") + rules.Add("!agentuity.yaml") + assert.False(t, rules.Ignore(".agentuity/foo.py", nil)) + assert.False(t, rules.Ignore("agentuity.yaml", nil)) + assert.True(t, rules.Ignore("bar.py", nil)) +} + +func TestFullWildcardRulesFilteredOut(t *testing.T) { + rules := Empty() + rules.AddDefaults() + rules.Add("agentuity.yaml") + rules.Add("!.agentuity/**") + rules.Add("**/*") + rules.Add("!agentuity.yaml") + assert.False(t, rules.Ignore(".agentuity/foo.py", nil)) + assert.False(t, rules.Ignore("agentuity.yaml", nil)) + assert.True(t, rules.Ignore("bar.py", nil)) +}