Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
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
1 change: 1 addition & 0 deletions cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
69 changes: 59 additions & 10 deletions internal/ignore/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*")
Expand Down Expand Up @@ -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
}
Expand All @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
54 changes: 54 additions & 0 deletions internal/ignore/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Loading