Skip to content

Add glob-pattern support for --exclude and integrate matcher into watcher/syncer#6

Draft
capcom6 wants to merge 4 commits intomasterfrom
codex/plan-implementation-for-roadmap-feature
Draft

Add glob-pattern support for --exclude and integrate matcher into watcher/syncer#6
capcom6 wants to merge 4 commits intomasterfrom
codex/plan-implementation-for-roadmap-feature

Conversation

@capcom6
Copy link
Copy Markdown
Owner

@capcom6 capcom6 commented Apr 16, 2026

Motivation

  • Provide flexible glob-based excludes (supports *, **, ?) so users can ignore groups of files like node_modules/** or **/*.tmp instead of enumerating paths.
  • Centralize and precompile exclude rules to avoid repeated work in hot paths and make behavior predictable across initial sync, create/update, and delete events.
  • Update docs and CLI help to advertise the new pattern semantics and ship a short implementation plan for the feature.

Description

  • Add internal/exclude/matcher.go which implements New and Match/MatchRule using github.com/bmatcuk/doublestar/v4 for doublestar-style glob matching and path normalization.
  • Wire the matcher into the CLI flow by building it in internal/cli/commands/sync/sync.go and updating main.go and flag help text to mention glob support.
  • Integrate exclusion checks into internal/watcher and internal/syncer so watcher registration and sync operations early-return for excluded paths and log which rule caused the skip.
  • Add documentation and examples in README.md, add docs/implementation-plan-exclude-patterns.md, update go.mod with the new dependency, and include unit tests in internal/exclude/matcher_test.go covering literal and pattern cases plus normalization and absolute-path boundaries.

Testing

  • Unit tests added for the matcher were run with go test ./internal/exclude -v and passed.
  • Repository-wide tests and build were validated with go test ./... and go build ./... and succeeded.

Codex Task

Summary by CodeRabbit

  • New Features

    • Exclude option now supports glob patterns (*, **, ?) and is enforced during sync and watch so excluded files/dirs are skipped.
  • Documentation

    • README updated to document glob pattern support for --exclude; roadmap item marked complete.
  • Tests

    • Added tests for literal vs pattern behavior, path normalization, and root/absolute constraints.
  • Chores

    • Added a glob-matching dependency to support pattern handling.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 16, 2026

🤖 Pull request artifacts

Platform File
🐳 Docker GitHub Container Registry
🍎 Darwin arm64 sftp-sync_Darwin_arm64.tar.gz
🍎 Darwin x86_64 sftp-sync_Darwin_x86_64.tar.gz
🐧 Linux arm64 sftp-sync_Linux_arm64.tar.gz
🐧 Linux i386 sftp-sync_Linux_i386.tar.gz
🐧 Linux x86_64 sftp-sync_Linux_x86_64.tar.gz
🪟 Windows arm64 sftp-sync_Windows_arm64.zip
🪟 Windows i386 sftp-sync_Windows_i386.zip
🪟 Windows x86_64 sftp-sync_Windows_x86_64.zip

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Walkthrough

Adds an internal/exclude.Matcher for glob-aware exclude rules (supports *, **, ?), pins doublestar v4, wires the matcher into watcher and syncer workflows to skip matched paths, updates CLI/docs usage text, and adds unit tests for matcher behavior.

Changes

Cohort / File(s) Summary
Documentation
README.md, main.go
Clarified --exclude accepts glob patterns (*, **, ?) and marked roadmap item complete; updated flag usage text.
Dependency Management
go.mod
Added github.com/bmatcuk/doublestar/v4 v4.10.0 as a direct dependency.
Exclude package
internal/exclude/errors.go, internal/exclude/matcher.go
New exclude package: ErrInvalidPattern and Matcher with New([]string, sourceRoot), Match, and MatchRule; normalizes paths, validates doublestar patterns, distinguishes literal vs pattern rules, and enforces source-root constraints.
Exclude tests
internal/exclude/matcher_test.go
New unit tests covering invalid patterns, literal vs pattern semantics, ancestor/descendant matching, recursive and segment patterns, and root-constrained absolute-path matching.
CLI wiring
internal/cli/commands/sync/sync.go
Constructs exclude.Matcher from cfg.Excludes (errors on invalid patterns) and passes matcher into watcher.New and syncer.New; updated flag help text.
Syncer integration
internal/syncer/syncer.go
Syncer.New signature updated to accept *exclude.Matcher; adds isExcluded checks to skip excluded paths early during sync and traversal.
Watcher integration
internal/watcher/watcher.go
Watcher.New signature updated to accept *exclude.Matcher; removed absolute-excludes handling and centralized exclusion via matcher with logging of matched rule.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI
  participant Exclude as "exclude.Matcher"
  participant Watcher
  participant Syncer
  participant FS as "Filesystem / fsnotify"

  CLI->>Exclude: New(cfg.Excludes, cfg.Source)
  Exclude-->>CLI: matcher / error
  CLI->>Watcher: New(rootPath, matcher, logger)
  CLI->>Syncer: New(rootPath, client, matcher, logger)

  FS->>Watcher: fsnotify event (path)
  Watcher->>Exclude: MatchRule(fullpath)
  Exclude-->>Watcher: matched? (rule)
  alt matched
    Watcher-->>Watcher: log debug + skip
  else not matched
    Watcher->>Syncer: notify event/path
    Syncer->>Exclude: MatchRule(absPath) or MatchRule(relPath)
    Exclude-->>Syncer: matched?
    alt matched
      Syncer-->>Syncer: log debug + skip/return
    else not matched
      Syncer->>FS: upload/remove/reconcile actions
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding glob-pattern support to the --exclude option and integrating the matcher into watcher/syncer components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/plan-implementation-for-roadmap-feature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (10)
internal/syncer/syncer.go (1)

159-169: Simplify isExcluded – fallback is unnecessary.

The matcher's normalizeInputPath already handles both absolute and relative paths (converting absolute paths to relative internally). Calling MatchRule twice with both absPath and relPath is redundant—passing just absPath should suffice.

♻️ Suggested simplification
 func (s *Syncer) isExcluded(absPath, relPath string, isDir bool) (bool, string) {
 	if s.matcher == nil {
 		return false, ""
 	}
 
-	if matched, rule := s.matcher.MatchRule(absPath, isDir); matched {
-		return true, rule
-	}
-
-	return s.matcher.MatchRule(relPath, isDir)
+	return s.matcher.MatchRule(absPath, isDir)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/syncer/syncer.go` around lines 159 - 169, The isExcluded method
redundantly calls s.matcher.MatchRule twice; simplify by only using the absolute
path once. Keep the nil check for s.matcher, then call
s.matcher.MatchRule(absPath, isDir) and return its (matched, rule) result
directly instead of calling MatchRule(relPath...). Update the function body in
Syncer.isExcluded to remove the fallback call and return the single MatchRule
result.
internal/exclude/matcher.go (3)

61-61: Unused isDir parameter.

The isDir parameter is ignored (blank identifier _ bool). If directory-specific matching behavior is planned (e.g., patterns ending with /), consider implementing it; otherwise, remove the parameter from the API to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` at line 61, The MatchRule method currently
declares an unused isDir parameter (signature: Matcher.MatchRule(filePath
string, _ bool) (bool, string)); remove the unused boolean parameter from the
method signature (change to Matcher.MatchRule(filePath string) (bool, string))
and update all callers and any tests to use the new signature, or alternatively
implement directory-aware logic inside Matcher.MatchRule to respect patterns
ending with '/' by using the isDir value — pick one approach consistently and
adjust references to the Matcher.MatchRule symbol accordingly.

12-16: Consider simplifying the rule struct.

Both literal and pattern fields are always assigned the same normalized value (lines 44-45). You could consolidate into a single value field to reduce redundancy.

♻️ Suggested simplification
 type rule struct {
-	literal   string
-	pattern   string
+	value     string
 	isPattern bool
 }

Then update usages accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 12 - 16, The rule struct has
redundant fields literal and pattern that are always the same; replace them with
a single field (e.g., value string) and keep isPattern bool to indicate whether
value is a glob/pattern or a literal, then update every place that constructs or
reads rule.literal or rule.pattern (e.g., parsers/constructors and matchers) to
use rule.value instead and preserve the existing isPattern semantics so matching
logic in functions that reference rule.literal/pattern continues to work
correctly.

125-127: Consider documenting the intentional omission of [ and ].

The hasMeta function only checks for * and ?, meaning bracket expressions like [abc] are treated as literals. This appears intentional (per the tests), but a brief comment would clarify this design choice for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 125 - 127, The hasMeta function
currently checks only for '*' and '?' via strings.ContainsAny(pattern, "*?") and
intentionally treats bracket expressions like '[' and ']' as literal characters;
add a brief comment above hasMeta explaining this design decision (i.e., that
'[' and ']' are intentionally omitted so character classes are not considered
meta) and reference the function name hasMeta and the pattern parameter so
future maintainers understand why bracket expressions are not treated as meta
patterns.
internal/watcher/watcher.go (1)

227-230: Consider threading context through addRecursive.

Using context.Background() here loses the ability to respect cancellation during initial directory traversal and prevents structured logging context from propagating. Consider passing the context from the caller.

♻️ Suggested change
-func (w *Watcher) addRecursive(path string) error {
-	if w.isExcluded(context.Background(), path) {
+func (w *Watcher) addRecursive(ctx context.Context, path string) error {
+	if w.isExcluded(ctx, path) {
 		return nil
 	}

Then update callers to pass ctx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/watcher/watcher.go` around lines 227 - 230, The call to
context.Background() in addRecursive loses cancellation and logging context;
change the signature of Watcher.addRecursive to accept a context (e.g., func (w
*Watcher) addRecursive(ctx context.Context, path string) error) and use that ctx
when calling w.isExcluded and any other context-using helpers, then update all
callers of addRecursive to pass through their ctx (or derive one) so
cancellation and structured logging propagate correctly.
docs/implementation-plan-exclude-patterns.md (5)

54-60: Clarify matching order and literal-vs-pattern precedence.

The statement "Every --exclude entry is treated as: 1. literal path match... 2. glob pattern match" is ambiguous. Does this mean:

  • Both checks are performed for every rule (literal first, then pattern)?
  • Or that each rule is classified as either literal OR pattern?

If both are attempted, what's the performance impact? If a rule contains glob metacharacters, should literal matching be skipped? Clarify the evaluation order and short-circuit behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/implementation-plan-exclude-patterns.md` around lines 54 - 60, Clarify
that each --exclude entry is evaluated deterministically: first test for an
exact literal path equality (short-circuit and treat as a match if true), and
only if the literal check fails then evaluate as a glob pattern match;
explicitly note that entries containing glob metacharacters still undergo the
literal check first but that implementations MAY skip the literal check for
strings that are explicitly flagged/parsed as patterns to avoid extra cost, and
document that a successful literal match prevents any subsequent glob evaluation
(short-circuit) and that directory literal matches exclude all descendants.

43-67: Add cross-platform and edge-case considerations.

The proposed behavior should explicitly address:

  1. Case sensitivity: Should pattern matching be case-sensitive? This varies by platform (Windows is case-insensitive, Unix is case-sensitive). Specify the intended behavior or document that it follows the underlying filesystem.
  2. Symbolic links: How should symbolic links be handled? Should they be followed for matching, or matched by their link name?
  3. Absolute paths: What happens if a user provides an absolute path in a pattern (e.g., /home/user/project/file.txt)? Should these be rejected, normalized, or supported?
  4. Directory matching scope (line 59): Explicitly state whether "directory match excludes all descendants" applies to both literal matches (e.g., node_modules) and pattern matches (e.g., **/tmp).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/implementation-plan-exclude-patterns.md` around lines 43 - 67, Update
the "Proposed behavior" section to explicitly cover cross-platform and edge
cases: state the intended case-sensitivity behavior (e.g., normalize decision or
"follows underlying filesystem"), define symbolic link handling (e.g., whether
matching follows symlinks or matches the link name), specify how absolute paths
in patterns are treated (reject, normalize to source-root-relative, or support
as-is), and clarify that the rule "Directory match excludes all descendants" in
"Matching rules" applies to both literal matches and glob pattern matches (give
examples like `node_modules` and `**/tmp` to illustrate). Ensure these
clarifications appear alongside the existing "Pattern syntax" and "Matching
rules" text so readers can find them with the same headings and examples (e.g.,
`--exclude=.git/**`, `--exclude=**/*.tmp`).

106-115: Expand test coverage to include additional edge cases.

The minimum test cases are solid. Consider adding:

  • Case sensitivity: Test behavior with patterns like *.TMP vs *.tmp across platforms.
  • Malformed patterns: Test validation of invalid patterns (e.g., unbalanced brackets if using extended globs later).
  • Broad patterns: Test performance implications of overly broad patterns like **/* or *.
  • Empty/whitespace rules: Test behavior with empty strings or whitespace-only exclude rules.
  • Boundary cases: Test patterns matching the source root itself.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/implementation-plan-exclude-patterns.md` around lines 106 - 115, Add
additional unit tests around the exclude-pattern matcher to cover edge cases:
add tests for case-sensitivity (e.g., patterns like "*.TMP" vs "*.tmp"),
malformed patterns (invalid glob syntax), broad patterns ("**/*" and "*" to
observe matching and performance), empty or whitespace-only rules, and patterns
that match the source root itself; target the existing exclude matcher tests
(the test file covering the exclude logic / the function that evaluates excludes
e.g., isExcluded or matchesExclude) and assert normalized path handling (Windows
separators) and explicit validation errors for invalid patterns where
applicable.

150-155: Optionally enhance acceptance criteria with validation tooling.

The acceptance criteria are clear and measurable. Consider adding:

  • Optional enhancement: "Users can run a --validate-excludes or --list-excluded command to preview which files would be excluded without performing actual sync, reducing risk of unintended exclusions."

This would strengthen the UX and make troubleshooting easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/implementation-plan-exclude-patterns.md` around lines 150 - 155, Add an
optional acceptance criterion that describes a dry-run/preview validation tool
(e.g., a CLI flag like --validate-excludes or --list-excluded) that allows users
to preview which files would be excluded without performing create/update/delete
actions; update the "Acceptance criteria" section to include this bullet,
clarify it is optional/enhancement, and ensure it specifies expected behavior
(preview output, non-destructive, usable with glob and literal excludes).

137-147: Consider additional risks for completeness.

The identified risks are good. Consider adding:

  1. Unintended critical file exclusion: Users might write overly broad patterns (e.g., *.log) that accidentally exclude important files.

    • Mitigation: Provide a --dry-run or --list-excluded flag to preview what would be excluded before actual sync.
  2. Literal filenames with glob metacharacters: If a user has a file literally named test[1].txt and wants to exclude it, the current plan treats all rules as potential patterns. A rule test[1].txt might be interpreted as a character class pattern rather than a literal filename.

    • Mitigation: Document escaping mechanism (if supported by doublestar) or recommend a prefix syntax like literal: to force literal matching.
  3. Production rollback: If severe issues arise post-release, what's the rollback strategy?

    • Mitigation: Consider a feature flag or environment variable to temporarily disable pattern matching and fall back to literal-only mode.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/implementation-plan-exclude-patterns.md` around lines 137 - 147, Add
three additional risk items under the "Risks and mitigations" section: 4)
Unintended critical file exclusion — advise adding a preview flag like --dry-run
or --list-excluded to show what would be excluded before syncing; 5) Literal
filenames with glob metacharacters — document escaping rules for doublestar or
introduce/describe a literal: prefix to force literal matching; and 6)
Production rollback — specify a rollback strategy such as a feature flag or
environment variable to disable pattern matching and fall back to literal-only
mode in emergencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@go.mod`:
- Line 6: Update the doublestar dependency from v4.8.1 to v4.10.0 in go.mod by
changing the module version string (the line containing
"github.com/bmatcuk/doublestar/v4 v4.8.1"), then run `go get
github.com/bmatcuk/doublestar/v4@v4.10.0` followed by `go mod tidy` to refresh
the lockfile and verify module resolution; finally run the test suite/build to
ensure no breaking changes affected code that uses doublestar functions.

In `@internal/exclude/matcher_test.go`:
- Around line 87-95: The test loop contains a redundant per-iteration copy `tt
:= tt` causing a copyloopvar lint; remove that line and use the ranged variable
`tt` directly in the t.Run closure, leaving the call to matcher.Match(tt.path,
false) and the t.Fatalf unchanged so each subtest uses the correct tt values
(i.e., update the loop that iterates over tests and the t.Run anonymous function
to drop the extra `tt := tt`).
- Around line 39-47: The test loop contains a redundant loop-variable copy `tt
:= tt` inside the range over `tests`; remove that assignment so each subtest
uses the iteration-scoped `tt` directly. Update the for loop that ranges over
`tests` and its t.Run closure (which calls matcher.Match) by deleting the `tt :=
tt` line to satisfy Go 1.22+ semantics and the linter warning.

In `@internal/syncer/syncer.go`:
- Around line 110-111: Replace path.Join with filepath.Join when constructing
childAbsPath and childRelPath (these variables are created using absPath and
relPath) so filesystem separators follow the OS semantics; update the same
usages of path.Join elsewhere in this function (the later construction of
sibling/descendant paths around where childAbsPath/childRelPath are set) to
filepath.Join to avoid mixing path and filepath operations.

In `@README.md`:
- Around line 217-222: The "Proposed next implementation" heading and its text
now conflict with the checklist state showing the `--exclude` patterns feature
as completed; update the README's "Proposed next implementation" section
(heading and the bullet referencing
`docs/implementation-plan-exclude-patterns.md`) to either (a) mark it as
completed and move or rename the section to something like "Completed: Support
for patterns in the `--exclude` option" with a brief status note, or (b) remove
the section entirely to avoid duplication; ensure you reference the same heading
text "Proposed next implementation" and the linked file
`docs/implementation-plan-exclude-patterns.md` when making the change.

---

Nitpick comments:
In `@docs/implementation-plan-exclude-patterns.md`:
- Around line 54-60: Clarify that each --exclude entry is evaluated
deterministically: first test for an exact literal path equality (short-circuit
and treat as a match if true), and only if the literal check fails then evaluate
as a glob pattern match; explicitly note that entries containing glob
metacharacters still undergo the literal check first but that implementations
MAY skip the literal check for strings that are explicitly flagged/parsed as
patterns to avoid extra cost, and document that a successful literal match
prevents any subsequent glob evaluation (short-circuit) and that directory
literal matches exclude all descendants.
- Around line 43-67: Update the "Proposed behavior" section to explicitly cover
cross-platform and edge cases: state the intended case-sensitivity behavior
(e.g., normalize decision or "follows underlying filesystem"), define symbolic
link handling (e.g., whether matching follows symlinks or matches the link
name), specify how absolute paths in patterns are treated (reject, normalize to
source-root-relative, or support as-is), and clarify that the rule "Directory
match excludes all descendants" in "Matching rules" applies to both literal
matches and glob pattern matches (give examples like `node_modules` and `**/tmp`
to illustrate). Ensure these clarifications appear alongside the existing
"Pattern syntax" and "Matching rules" text so readers can find them with the
same headings and examples (e.g., `--exclude=.git/**`, `--exclude=**/*.tmp`).
- Around line 106-115: Add additional unit tests around the exclude-pattern
matcher to cover edge cases: add tests for case-sensitivity (e.g., patterns like
"*.TMP" vs "*.tmp"), malformed patterns (invalid glob syntax), broad patterns
("**/*" and "*" to observe matching and performance), empty or whitespace-only
rules, and patterns that match the source root itself; target the existing
exclude matcher tests (the test file covering the exclude logic / the function
that evaluates excludes e.g., isExcluded or matchesExclude) and assert
normalized path handling (Windows separators) and explicit validation errors for
invalid patterns where applicable.
- Around line 150-155: Add an optional acceptance criterion that describes a
dry-run/preview validation tool (e.g., a CLI flag like --validate-excludes or
--list-excluded) that allows users to preview which files would be excluded
without performing create/update/delete actions; update the "Acceptance
criteria" section to include this bullet, clarify it is optional/enhancement,
and ensure it specifies expected behavior (preview output, non-destructive,
usable with glob and literal excludes).
- Around line 137-147: Add three additional risk items under the "Risks and
mitigations" section: 4) Unintended critical file exclusion — advise adding a
preview flag like --dry-run or --list-excluded to show what would be excluded
before syncing; 5) Literal filenames with glob metacharacters — document
escaping rules for doublestar or introduce/describe a literal: prefix to force
literal matching; and 6) Production rollback — specify a rollback strategy such
as a feature flag or environment variable to disable pattern matching and fall
back to literal-only mode in emergencies.

In `@internal/exclude/matcher.go`:
- Line 61: The MatchRule method currently declares an unused isDir parameter
(signature: Matcher.MatchRule(filePath string, _ bool) (bool, string)); remove
the unused boolean parameter from the method signature (change to
Matcher.MatchRule(filePath string) (bool, string)) and update all callers and
any tests to use the new signature, or alternatively implement directory-aware
logic inside Matcher.MatchRule to respect patterns ending with '/' by using the
isDir value — pick one approach consistently and adjust references to the
Matcher.MatchRule symbol accordingly.
- Around line 12-16: The rule struct has redundant fields literal and pattern
that are always the same; replace them with a single field (e.g., value string)
and keep isPattern bool to indicate whether value is a glob/pattern or a
literal, then update every place that constructs or reads rule.literal or
rule.pattern (e.g., parsers/constructors and matchers) to use rule.value instead
and preserve the existing isPattern semantics so matching logic in functions
that reference rule.literal/pattern continues to work correctly.
- Around line 125-127: The hasMeta function currently checks only for '*' and
'?' via strings.ContainsAny(pattern, "*?") and intentionally treats bracket
expressions like '[' and ']' as literal characters; add a brief comment above
hasMeta explaining this design decision (i.e., that '[' and ']' are
intentionally omitted so character classes are not considered meta) and
reference the function name hasMeta and the pattern parameter so future
maintainers understand why bracket expressions are not treated as meta patterns.

In `@internal/syncer/syncer.go`:
- Around line 159-169: The isExcluded method redundantly calls
s.matcher.MatchRule twice; simplify by only using the absolute path once. Keep
the nil check for s.matcher, then call s.matcher.MatchRule(absPath, isDir) and
return its (matched, rule) result directly instead of calling
MatchRule(relPath...). Update the function body in Syncer.isExcluded to remove
the fallback call and return the single MatchRule result.

In `@internal/watcher/watcher.go`:
- Around line 227-230: The call to context.Background() in addRecursive loses
cancellation and logging context; change the signature of Watcher.addRecursive
to accept a context (e.g., func (w *Watcher) addRecursive(ctx context.Context,
path string) error) and use that ctx when calling w.isExcluded and any other
context-using helpers, then update all callers of addRecursive to pass through
their ctx (or derive one) so cancellation and structured logging propagate
correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e909d99-0b8d-4891-a306-715d1d6e90af

📥 Commits

Reviewing files that changed from the base of the PR and between f8d412f and 241ece7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • README.md
  • docs/implementation-plan-exclude-patterns.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/exclude/matcher.go
  • internal/exclude/matcher_test.go
  • internal/syncer/syncer.go
  • internal/watcher/watcher.go
  • main.go

Comment thread go.mod Outdated
Comment thread internal/exclude/matcher_test.go
Comment thread internal/exclude/matcher_test.go
Comment thread internal/syncer/syncer.go Outdated
Comment thread README.md Outdated
@capcom6 capcom6 force-pushed the codex/plan-implementation-for-roadmap-feature branch from 241ece7 to f399f7f Compare April 16, 2026 11:39
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/exclude/matcher.go`:
- Around line 23-33: The New function currently calls normalizePath on each raw
rule which strips leading slashes and misinterprets absolute rules; update New
to detect absolute rules (using filepath.IsAbs) and handle them explicitly: if a
rule is absolute, convert it to a path relative to absSourceRoot (use
filepath.Rel(absSourceRoot, raw)) and reject with an error if the absolute rule
is outside sourceRoot, then pass the resulting relative path into normalizePath;
alternatively, if absolute rules should be unsupported, return a clear error
from New when filepath.IsAbs(raw) is true instead of silently normalizing them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0262868-53d4-40d1-8da6-644379d53485

📥 Commits

Reviewing files that changed from the base of the PR and between 241ece7 and f399f7f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • README.md
  • go.mod
  • internal/cli/commands/sync/sync.go
  • internal/exclude/matcher.go
  • internal/exclude/matcher_test.go
  • internal/syncer/syncer.go
  • internal/watcher/watcher.go
  • main.go
✅ Files skipped from review due to trivial changes (1)
  • main.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • README.md
  • go.mod

Comment thread internal/exclude/matcher.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
internal/exclude/matcher.go (3)

79-93: Minor: avoid building the ancestor list when no pattern rules exist, and skip the literal-prefix check for pattern rules.

Two small tweaks on the hot path (every sync/watch event calls this):

  1. pathCandidates walks the path for every call, even when all rules are literal.
  2. For a pattern rule like **/*.tmp, the normalized == r.literal || HasPrefix(normalized, r.literal+"/") check on line 81 can never succeed and is pure overhead.
Proposed tweak
-	ancestors := pathCandidates(normalized)
-	for _, r := range m.rules {
-		if normalized == r.literal || strings.HasPrefix(normalized, r.literal+"/") {
-			return true, r.literal
-		}
-
-		if r.isPattern {
-			for _, candidate := range ancestors {
-				matched, err := doublestar.Match(r.pattern, candidate)
-				if err == nil && matched {
-					return true, r.pattern
-				}
-			}
-		}
-	}
+	var ancestors []string
+	for _, r := range m.rules {
+		if !r.isPattern {
+			if normalized == r.literal || strings.HasPrefix(normalized, r.literal+"/") {
+				return true, r.literal
+			}
+			continue
+		}
+		if ancestors == nil {
+			ancestors = pathCandidates(normalized)
+		}
+		for _, candidate := range ancestors {
+			if matched, err := doublestar.Match(r.pattern, candidate); err == nil && matched {
+				return true, r.pattern
+			}
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 79 - 93, The pathCandidates list is
built unconditionally and the literal-prefix check runs for pattern rules;
change the logic in the match loop to: first, detect whether any m.rules have
r.isPattern and only call pathCandidates(normalized) when at least one pattern
exists; second, for each rule skip the literal equality/prefix check when
r.isPattern is true (only perform normalized == r.literal ||
strings.HasPrefix(normalized, r.literal+"/") for non-pattern rules), and keep
the existing doublestar.Match loop for pattern rules (returning r.pattern on
match) so literal rules still return r.literal immediately.

12-16: Redundant literal/pattern fields on rule.

Both fields are always assigned the same normalized value, so rule carries duplicated state. A single value string (plus isPattern) keeps the struct smaller and the intent clearer.

Proposed simplification
 type rule struct {
-	literal   string
-	pattern   string
-	isPattern bool
+	value     string
+	isPattern bool
 }
@@
-		compiled = append(compiled, rule{
-			literal:   normalized,
-			pattern:   normalized,
-			isPattern: isPattern,
-		})
+		compiled = append(compiled, rule{
+			value:     normalized,
+			isPattern: isPattern,
+		})

Update MatchRule accordingly (r.value in both the literal-prefix check and the doublestar.Match call).

Also applies to: 55-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 12 - 16, The rule struct currently
duplicates state with literal and pattern both set to the same normalized value;
replace these two fields with a single value string plus the existing isPattern
bool, update all usages (including the MatchRule function) to read r.value for
both the literal-prefix check and the doublestar.Match call, and remove the
redundant fields and any code that sets both literal and pattern so the struct
and related logic only store and use r.value and r.isPattern.

73-73: isDir parameter is accepted but ignored.

Both Match and MatchRule take an isDir bool, callers in internal/syncer/syncer.go and internal/watcher/watcher.go pass it, but the implementation discards it via _ bool. Either:

  • drop the parameter from the public API to avoid misleading callers into thinking directory-specific rules (e.g., trailing-slash rules à la .gitignore) are honored, or
  • actually use it — e.g., only allow a rule ending in / to match when isDir is true, which also restores the "match directories only" semantics lost to filepath.Clean on line 31.

Given the watcher currently hard-codes false on line 261 of internal/watcher/watcher.go, whichever path you pick, the caller contract should be made explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` at line 73, Matcher.Match and Matcher.MatchRule
currently accept an isDir bool but ignore it (signature uses `_ bool`), so
either remove the parameter from both APIs or honor it; to fix, decide on one
approach and make caller contract explicit: either remove isDir from
Matcher.Match and Matcher.MatchRule signatures and update all callers to stop
passing it, or implement directory-only matching (e.g., treat rules ending with
'/' as matching only when isDir is true and preserve other semantics lost by
filepath.Clean) by using the passed isDir in both Matcher.Match and
Matcher.MatchRule and update the caller that currently passes a hard-coded false
to pass the actual directory flag. Ensure you update both methods
(Matcher.Match, Matcher.MatchRule) and all callers to be consistent with the
chosen contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/exclude/matcher.go`:
- Line 31: The code uses filepath.Clean on strings.TrimSpace(raw) which silently
removes a trailing slash and thus loses directory-only intent; preserve that
intent by detecting whether the original raw string (before TrimSpace/Clean)
ends with a path separator (e.g. strings.HasSuffix(raw, "/") or
os.PathSeparator) and carry that flag forward instead of letting filepath.Clean
drop it; update the logic around the cleanedRaw assignment (the
filepath.Clean(strings.TrimSpace(raw)) expression) and downstream handling that
currently ignores the isDir parameter (see the comment near line 73) so that
directory-only matches are honored when the original pattern had a trailing
slash, and ensure any canonicalization still uses cleanedRaw for normalization
while relying on the new flag for directory-only semantics.

---

Nitpick comments:
In `@internal/exclude/matcher.go`:
- Around line 79-93: The pathCandidates list is built unconditionally and the
literal-prefix check runs for pattern rules; change the logic in the match loop
to: first, detect whether any m.rules have r.isPattern and only call
pathCandidates(normalized) when at least one pattern exists; second, for each
rule skip the literal equality/prefix check when r.isPattern is true (only
perform normalized == r.literal || strings.HasPrefix(normalized, r.literal+"/")
for non-pattern rules), and keep the existing doublestar.Match loop for pattern
rules (returning r.pattern on match) so literal rules still return r.literal
immediately.
- Around line 12-16: The rule struct currently duplicates state with literal and
pattern both set to the same normalized value; replace these two fields with a
single value string plus the existing isPattern bool, update all usages
(including the MatchRule function) to read r.value for both the literal-prefix
check and the doublestar.Match call, and remove the redundant fields and any
code that sets both literal and pattern so the struct and related logic only
store and use r.value and r.isPattern.
- Line 73: Matcher.Match and Matcher.MatchRule currently accept an isDir bool
but ignore it (signature uses `_ bool`), so either remove the parameter from
both APIs or honor it; to fix, decide on one approach and make caller contract
explicit: either remove isDir from Matcher.Match and Matcher.MatchRule
signatures and update all callers to stop passing it, or implement
directory-only matching (e.g., treat rules ending with '/' as matching only when
isDir is true and preserve other semantics lost by filepath.Clean) by using the
passed isDir in both Matcher.Match and Matcher.MatchRule and update the caller
that currently passes a hard-coded false to pass the actual directory flag.
Ensure you update both methods (Matcher.Match, Matcher.MatchRule) and all
callers to be consistent with the chosen contract.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ead20beb-99eb-4dfa-93ca-b31c0a4032fe

📥 Commits

Reviewing files that changed from the base of the PR and between f399f7f and 5875589.

📒 Files selected for processing (4)
  • internal/exclude/errors.go
  • internal/exclude/matcher.go
  • internal/exclude/matcher_test.go
  • internal/syncer/syncer.go
✅ Files skipped from review due to trivial changes (1)
  • internal/exclude/matcher_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/syncer/syncer.go

Comment thread internal/exclude/matcher.go Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • internal/exclude/matcher.go

Commit: 173f1aa0a312730038c2d2026f73a8e5473e6ef9

The changes have been pushed to the codex/plan-implementation-for-roadmap-feature branch.

Time taken: 4m 17s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/exclude/matcher.go (1)

147-149: hasMeta misses doublestar's character-class and brace metacharacters.

doublestar treats [, ], {, } as glob metacharacters, but hasMeta only detects *?. A rule like file[0-9].log or {a,b}/c returns false from hasMeta, causing the pattern to skip validation and be treated as a literal match—silently failing to match as a glob pattern.

♻️ Suggested change
 func hasMeta(pattern string) bool {
-	return strings.ContainsAny(pattern, "*?")
+	// Mirror the metacharacters recognized by doublestar: *, ?, [..], {..}.
+	return strings.ContainsAny(pattern, "*?[{")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 147 - 149, hasMeta currently only
checks for "*" and "?" using strings.ContainsAny, but doublestar also treats
"[", "]", "{", "}" as glob metacharacters; update hasMeta to detect those
additional characters so patterns like "file[0-9].log" or "{a,b}/c" are
recognized as globs. Modify the function hasMeta to include the bracket and
brace characters in the character set checked (keep using strings.ContainsAny or
equivalent) so it returns true for any of "*?[]{}". Ensure you update the code
paths that call hasMeta (e.g., the glob validation/branching logic) so they will
now treat those patterns as globs rather than literals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/exclude/matcher.go`:
- Line 164: The file internal/exclude/matcher.go is not correctly formatted
(EOF/newline or import grouping) causing golangci-lint to fail; fix by running
goimports -w internal/exclude/matcher.go (or gofmt -s -w
internal/exclude/matcher.go) to normalize imports and add the trailing newline,
then re-run the linter/CI; ensure the final commit includes the formatted
matcher.go changes.
- Around line 85-103: The directory-only guard in the loop over m.rules (in
matcher.go) is too broad: the check "if r.isDir && !isDir { continue }" prevents
descendant literal-prefix matches (e.g., r.literal "node_modules" should exclude
"node_modules/foo.js"); change the logic in the rule iteration so the r.isDir &&
!isDir branch only skips when the current path is an exact match of the rule
(compare normalized == r.literal), but still allow the literal-prefix check
(strings.HasPrefix(normalized, r.literal+"/")) to run for descendants; keep the
existing pattern handling (r.isPattern with doublestar.Match over ancestors)
unchanged.

---

Nitpick comments:
In `@internal/exclude/matcher.go`:
- Around line 147-149: hasMeta currently only checks for "*" and "?" using
strings.ContainsAny, but doublestar also treats "[", "]", "{", "}" as glob
metacharacters; update hasMeta to detect those additional characters so patterns
like "file[0-9].log" or "{a,b}/c" are recognized as globs. Modify the function
hasMeta to include the bracket and brace characters in the character set checked
(keep using strings.ContainsAny or equivalent) so it returns true for any of
"*?[]{}". Ensure you update the code paths that call hasMeta (e.g., the glob
validation/branching logic) so they will now treat those patterns as globs
rather than literals.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b991017-5b05-4c99-b283-f8f676a85632

📥 Commits

Reviewing files that changed from the base of the PR and between 5875589 and 173f1aa.

📒 Files selected for processing (1)
  • internal/exclude/matcher.go

Comment thread internal/exclude/matcher.go Outdated
Comment thread internal/exclude/matcher.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
internal/exclude/matcher.go (1)

29-44: ⚠️ Potential issue | 🟠 Major

Absolute exclude rules still silently misbehave.

filepath.ToSlash preserves the leading /, so a rule like /repo/dist/** is stored as /repo/dist/** and is then matched against cleaned relative paths like dist/foo.js — it will never match. Either relativize absolute rules against sourceRoot up front (rejecting rules outside the root), or reject absolute rules with a clear error from New. Also consider strings.TrimSpace and skipping empty entries so accidental blank lines in config don't produce a no-op rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 29 - 44, Trim each raw rule (use
strings.TrimSpace) and skip empty entries before calling filepath.ToSlash in the
code that builds the compiled []rule (the constructor New or the function that
creates compiled). After normalizing, detect absolute rules (leading "/") and
either relativize them against the package's sourceRoot parameter (rejecting and
returning an error if the absolute path is outside sourceRoot) or return a clear
ErrInvalidPattern-style error for absolute rules; update the compiled append
logic for rule{value: normalized, isPattern: hasMeta(normalized)} accordingly so
absolute rules no longer silently fail. Ensure validation still uses
doublestar.ValidatePattern and keep references to ErrInvalidPattern, rule,
hasMeta, and the New constructor for locating changes.
🧹 Nitpick comments (3)
internal/exclude/matcher.go (1)

36-38: Remove the commented-out ** prefixing block.

Dead/commented code should not ship — either re-enable it with a rationale or delete it. Leaving it in invites drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 36 - 38, Remove the dead commented
block that checks for a "**" prefix (the lines using
strings.HasPrefix(normalized, "**") and normalized = "/" + normalized) in
internal/exclude/matcher.go; either delete these commented lines entirely so no
commented-out logic remains around the normalized variable, or restore them as
active code with a short rationale comment if the "**" prefix handling is
required (update any related tests in functions referencing normalized
accordingly).
internal/syncer/syncer.go (2)

101-138: Deduplicate the per-entry exclusion block.

childAbsPath/childRelPath are constructed twice and the exclusion-check + debug-log pattern is copy-pasted across the dir and file branches. Hoisting it above the file.IsDir() switch tightens the loop and keeps the log/skip behavior consistent. Same pattern also appears in Sync (lines 49-55) and could be extracted into a small helper like skipIfExcluded(ctx, abs, rel string) bool that returns true after logging.

Proposed refactor
 	for _, file := range files {
 		select {
 		case <-ctx.Done():
 			return nil
 		default:
 		}
 
-		if file.IsDir() {
-			childAbsPath := filepath.Join(absPath, file.Name())
-			childRelPath := filepath.Join(relPath, file.Name())
-			if matched, rule := s.isExcluded(childAbsPath, childRelPath); matched {
-				s.logger.Debug(ctx, "Excluded path skipped", logger.Fields{
-					"path": childRelPath,
-					"rule": rule,
-				})
-				continue
-			}
-
-			if dErr := s.syncDir(ctx, childAbsPath, childRelPath); dErr != nil {
-				return dErr
-			}
-
-			continue
-		}
-
 		childAbsPath := filepath.Join(absPath, file.Name())
 		childRelPath := filepath.Join(relPath, file.Name())
 		if matched, rule := s.isExcluded(childAbsPath, childRelPath); matched {
 			s.logger.Debug(ctx, "Excluded path skipped", logger.Fields{
 				"path": childRelPath,
 				"rule": rule,
 			})
 			continue
 		}
 
+		if file.IsDir() {
+			if dErr := s.syncDir(ctx, childAbsPath, childRelPath); dErr != nil {
+				return dErr
+			}
+			continue
+		}
+
 		if fErr := s.syncFile(ctx, childAbsPath, childRelPath); fErr != nil {
 			return fErr
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/syncer/syncer.go` around lines 101 - 138, The loop duplicates
construction of childAbsPath/childRelPath and the exclusion-check+debug log in
both branches; hoist childAbsPath := filepath.Join(absPath, file.Name()) and
childRelPath := filepath.Join(relPath, file.Name()) to the top of the per-file
loop, call s.isExcluded(childAbsPath, childRelPath) once and continue if matched
(logging the rule), and then call s.syncDir(ctx, childAbsPath, childRelPath) or
s.syncFile(ctx, childAbsPath, childRelPath) based on file.IsDir(); also extract
the same pattern used in Sync into a small helper like skipIfExcluded(ctx, abs,
rel string) bool that calls s.isExcluded and s.logger.Debug so both syncer.go
loop and Sync use that helper to avoid duplication.

160-170: The relPath fallback is likely redundant.

MatchRule already converts absPath to a path relative to sourceRoot via filepath.Rel internally, so trying absPath first and then relPath double-normalizes. If the absolute-path call succeeds in relativization, the second call just repeats the same work; if it fails (path outside source), retrying with relPath won't produce a different match. Consider passing only absPath and dropping the fallback — or, if the fallback exists to paper over the Windows separator bug flagged in matcher.go, fixing that root cause is preferable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/syncer/syncer.go` around lines 160 - 170, The fallback to calling
s.matcher.MatchRule(relPath) in Syncer.isExcluded is redundant because MatchRule
already relativizes absPath; remove the second call and have isExcluded only
call and return the result of s.matcher.MatchRule(absPath) (i.e., change
isExcluded to call MatchRule(absPath) and return that bool/string), remove the
now-unused relPath parameter from isExcluded and update all callers accordingly;
if the fallback was intended to address the Windows-separator issue in
matcher.go, instead fix that normalization in matcher.MatchRule so relPath
fallback is unnecessary, then remove the fallback call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/exclude/matcher.go`:
- Around line 3-11: Replace the stdlib log usage and import with the injected
logger by adding a logger.Logger parameter to New (the matcher constructor used
by the watcher/syncer) and store it on the matcher struct, remove the "log"
import from internal/exclude/matcher.go, and change the noisy log.Printf calls
in MatchRule (and any other places) to use the stored logger at Debug level (or
remove them entirely) so linting passes and structured logging is used
consistently.
- Around line 57-67: The MatchRule function uses filepath.ToSlash on filePath
before calling filepath.Rel, causing separator mismatch with m.sourceRoot on
Windows; fix by first computing relPath := filepath.Rel(m.sourceRoot, filePath)
(use original OS separators), only if err == nil set normalized = relPath, then
convert normalized to forward slashes via filepath.ToSlash and then call
path.Clean; update the logic in Matcher.MatchRule (references:
Matcher.MatchRule, m.sourceRoot, normalized, filepath.Rel, filepath.ToSlash,
path.Clean) so relative paths are computed with OS separators and normalized
afterwards.

---

Duplicate comments:
In `@internal/exclude/matcher.go`:
- Around line 29-44: Trim each raw rule (use strings.TrimSpace) and skip empty
entries before calling filepath.ToSlash in the code that builds the compiled
[]rule (the constructor New or the function that creates compiled). After
normalizing, detect absolute rules (leading "/") and either relativize them
against the package's sourceRoot parameter (rejecting and returning an error if
the absolute path is outside sourceRoot) or return a clear
ErrInvalidPattern-style error for absolute rules; update the compiled append
logic for rule{value: normalized, isPattern: hasMeta(normalized)} accordingly so
absolute rules no longer silently fail. Ensure validation still uses
doublestar.ValidatePattern and keep references to ErrInvalidPattern, rule,
hasMeta, and the New constructor for locating changes.

---

Nitpick comments:
In `@internal/exclude/matcher.go`:
- Around line 36-38: Remove the dead commented block that checks for a "**"
prefix (the lines using strings.HasPrefix(normalized, "**") and normalized = "/"
+ normalized) in internal/exclude/matcher.go; either delete these commented
lines entirely so no commented-out logic remains around the normalized variable,
or restore them as active code with a short rationale comment if the "**" prefix
handling is required (update any related tests in functions referencing
normalized accordingly).

In `@internal/syncer/syncer.go`:
- Around line 101-138: The loop duplicates construction of
childAbsPath/childRelPath and the exclusion-check+debug log in both branches;
hoist childAbsPath := filepath.Join(absPath, file.Name()) and childRelPath :=
filepath.Join(relPath, file.Name()) to the top of the per-file loop, call
s.isExcluded(childAbsPath, childRelPath) once and continue if matched (logging
the rule), and then call s.syncDir(ctx, childAbsPath, childRelPath) or
s.syncFile(ctx, childAbsPath, childRelPath) based on file.IsDir(); also extract
the same pattern used in Sync into a small helper like skipIfExcluded(ctx, abs,
rel string) bool that calls s.isExcluded and s.logger.Debug so both syncer.go
loop and Sync use that helper to avoid duplication.
- Around line 160-170: The fallback to calling s.matcher.MatchRule(relPath) in
Syncer.isExcluded is redundant because MatchRule already relativizes absPath;
remove the second call and have isExcluded only call and return the result of
s.matcher.MatchRule(absPath) (i.e., change isExcluded to call MatchRule(absPath)
and return that bool/string), remove the now-unused relPath parameter from
isExcluded and update all callers accordingly; if the fallback was intended to
address the Windows-separator issue in matcher.go, instead fix that
normalization in matcher.MatchRule so relPath fallback is unnecessary, then
remove the fallback call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02fba150-69a4-4cdb-8413-71045c8dc19f

📥 Commits

Reviewing files that changed from the base of the PR and between 173f1aa and c2b2dd8.

📒 Files selected for processing (4)
  • internal/exclude/matcher.go
  • internal/exclude/matcher_test.go
  • internal/syncer/syncer.go
  • internal/watcher/watcher.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/exclude/matcher_test.go

Comment on lines +3 to +11
import (
"fmt"
"log"
"path"
"path/filepath"
"strings"

"github.com/bmatcuk/doublestar/v4"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace log with the injected logger to unblock CI.

depguard is failing the lint job on this import (import 'log' is not allowed from list 'non-main files'). The log.Printf calls at lines 66-67 also fire on every MatchRule invocation — once per watcher event and once per traversed entry — which is both noisy and unstructured. Take a logger.Logger in New (the watcher/syncer already thread one through) and drop these to Debug, or remove them entirely.

🧰 Tools
🪛 GitHub Actions: Go

[error] 5-5: golangci-lint (depguard): import 'log' is not allowed from list 'non-main files': Use log/slog instead (see https://go.dev/blog/slog).

🪛 GitHub Check: Lint

[failure] 5-5:
import 'log' is not allowed from list 'non-main files': Use log/slog instead, see https://go.dev/blog/slog (depguard)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 3 - 11, Replace the stdlib log
usage and import with the injected logger by adding a logger.Logger parameter to
New (the matcher constructor used by the watcher/syncer) and store it on the
matcher struct, remove the "log" import from internal/exclude/matcher.go, and
change the noisy log.Printf calls in MatchRule (and any other places) to use the
stored logger at Debug level (or remove them entirely) so linting passes and
structured logging is used consistently.

Comment on lines +57 to +67
func (m *Matcher) MatchRule(filePath string) (bool, string) {
normalized := filepath.ToSlash(filePath)
relPath, err := filepath.Rel(m.sourceRoot, normalized)
if err == nil {
normalized = relPath
}

normalized = path.Clean(normalized)

log.Printf("Normalizing %s to %s", filePath, normalized)
log.Printf("Matching %s against %v", normalized, m.rules)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "matcher.go" | head -20

Repository: capcom6/sftp-sync

Length of output: 90


🏁 Script executed:

cat -n ./internal/exclude/matcher.go

Repository: capcom6/sftp-sync

Length of output: 2505


🏁 Script executed:

grep -r "MatchRule\|Matcher" . --include="*.go" -n | grep -E "(test|Test)" | head -20

Repository: capcom6/sftp-sync

Length of output: 451


🏁 Script executed:

cat -n ./internal/exclude/matcher_test.go

Repository: capcom6/sftp-sync

Length of output: 3861


🏁 Script executed:

cat -n ./internal/exclude/matcher.go | sed -n '1,50p'

Repository: capcom6/sftp-sync

Length of output: 1364


🏁 Script executed:

go doc filepath.Rel

Repository: capcom6/sftp-sync

Length of output: 675


🏁 Script executed:

rg "filepath\.Rel" --type go -B 2 -A 2

Repository: capcom6/sftp-sync

Length of output: 1030


🏁 Script executed:

cat -n ./internal/watcher/watcher.go | grep -A 5 -B 5 "filepath.Rel"

Repository: capcom6/sftp-sync

Length of output: 391


🏁 Script executed:

cat -n ./internal/syncer/syncer.go | grep -A 5 -B 5 "filepath.Rel"

Repository: capcom6/sftp-sync

Length of output: 518


🏁 Script executed:

python3 << 'EOF'
import os
import sys

# Simulate what happens on Windows with filepath.Rel and mismatched separators
# This demonstrates the core issue

# On Windows:
# m.sourceRoot = "C:\repo" (from filepath.Abs)
# After ToSlash: "C:/repo" 
# filePath = "C:/repo/src/main.go" (after ToSlash)

# The issue is that filepath.Rel expects both arguments in the OS's native format
# When you mix separators, it can fail or produce unexpected results

print("===== Windows path separator mismatch scenario =====")
print()
print("Case 1: Correct (both with backslashes)")
print('  filepath.Rel("C:\\repo", "C:\\repo\\src\\main.go")')
print("  Result: src\\main.go (expected)")
print()
print("Case 2: Problematic (mismatched separators)")
print('  filepath.Rel("C:\\repo", "C:/repo/src/main.go")')
print("  Result: Error or bogus path like ..\\..\\C:\\repo\\src\\main.go")
print()
print("This is exactly what the code does currently:")
print("  1. normalized = filepath.ToSlash(filePath)  // converts to /")
print("  2. filepath.Rel(m.sourceRoot, normalized)  // m.sourceRoot has \\")
print()
print("The proposed fix addresses this by:")
print("  1. Computing relative path with OS separators first")
print("  2. Converting to forward slashes AFTER getting the relative path")
EOF

Repository: capcom6/sftp-sync

Length of output: 726


🏁 Script executed:

# Let me also check if there are any integration tests or how the code is tested
find . -type f -name "*test*.go" | xargs grep -l "Match\|Matcher" | head -5

Repository: capcom6/sftp-sync

Length of output: 95


Fix Windows path separator mismatch in filepath.Rel call.

On Windows, m.sourceRoot contains backslashes (from filepath.Abs at line 24), but normalized is converted to forward slashes via filepath.ToSlash at line 58. Calling filepath.Rel(m.sourceRoot, normalized) with mismatched separators will fail, causing the code to silently fall back to matching against absolute paths instead of relative ones. This breaks exclude rule matching on Windows.

Compute the relative path first using OS separators, then convert to forward slashes:

Proposed fix
-func (m *Matcher) MatchRule(filePath string) (bool, string) {
-	normalized := filepath.ToSlash(filePath)
-	relPath, err := filepath.Rel(m.sourceRoot, normalized)
-	if err == nil {
-		normalized = relPath
-	}
-
-	normalized = path.Clean(normalized)
-
-	log.Printf("Normalizing %s to %s", filePath, normalized)
-	log.Printf("Matching %s against %v", normalized, m.rules)
+func (m *Matcher) MatchRule(filePath string) (bool, string) {
+	candidate := filePath
+	if filepath.IsAbs(candidate) {
+		if rel, err := filepath.Rel(m.sourceRoot, candidate); err == nil {
+			candidate = rel
+		}
+	}
+	normalized := path.Clean(filepath.ToSlash(candidate))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/exclude/matcher.go` around lines 57 - 67, The MatchRule function
uses filepath.ToSlash on filePath before calling filepath.Rel, causing separator
mismatch with m.sourceRoot on Windows; fix by first computing relPath :=
filepath.Rel(m.sourceRoot, filePath) (use original OS separators), only if err
== nil set normalized = relPath, then convert normalized to forward slashes via
filepath.ToSlash and then call path.Clean; update the logic in Matcher.MatchRule
(references: Matcher.MatchRule, m.sourceRoot, normalized, filepath.Rel,
filepath.ToSlash, path.Clean) so relative paths are computed with OS separators
and normalized afterwards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant