Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] b70cba82ff fix(cloud): emit both - pattern and - pattern/** in rclone filter conversion
Previously `convert_bmignore_to_rclone_filters()` had two bugs:

1. Patterns without wildcards (e.g. `config.json`) were emitted as
   `- config.json/**`, which only excludes contents of a *directory*
   named config.json — the actual file passed through unfiltered.

2. Wildcard-only patterns (e.g. `.*`) were emitted as `- .*` alone.
   rclone globs treat `*` as not crossing `/`, so hidden directory
   contents (e.g. `.obsidian/workspace.json`) were not excluded.

Fix: always emit both `- pattern` (match the item itself) and
`- pattern/**` (match directory contents recursively) for every
gitignore line, unless it already ends with `/**`.

Closes #819

Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-05-14 00:05:59 +00:00
2 changed files with 58 additions and 8 deletions
@@ -78,14 +78,25 @@ def convert_bmignore_to_rclone_filters() -> Path:
patterns.append(line)
continue
# Convert gitignore pattern to rclone filter syntax
# gitignore: node_modules → rclone: - node_modules/**
# gitignore: *.pyc → rclone: - *.pyc
if "*" in line:
# Pattern already has wildcard, just add exclude prefix
# Convert gitignore pattern to rclone filter syntax.
#
# Two rules are always emitted per pattern:
# 1. `- pattern` — excludes the item itself (file or dir entry)
# 2. `- pattern/**` — excludes all contents when the item is a dir
#
# Why both? rclone globs treat `*` as not crossing `/`, so:
# - `- .*` matches hidden *files* but NOT hidden dir contents
# (e.g. `.obsidian/workspace.json` would pass through)
# - `- config.json/**` excludes contents of a *dir* named
# config.json, but NOT the file config.json itself
# Emitting both forms covers files, directories, and directory contents.
if line.endswith("/**"):
# Trigger: pattern already fully qualified as recursive dir exclude
# Why: avoid doubling up on /** suffix
# Outcome: written as-is
patterns.append(f"- {line}")
else:
# Directory pattern - add /** for recursive exclude
patterns.append(f"- {line}")
patterns.append(f"- {line}/**")
except Exception:
@@ -32,13 +32,52 @@ def test_convert_bmignore_to_rclone_filters_creates_and_converts(config_home):
# Comments/empties preserved
assert "# comment" in content
assert "" in content
# Directory pattern becomes recursive exclude
# Each pattern emits both a direct-match and a recursive-contents rule
assert "- node_modules" in content
assert "- node_modules/**" in content
# Wildcard pattern becomes simple exclude
assert "- *.pyc" in content
assert "- *.pyc/**" in content
assert "- .git" in content
assert "- .git/**" in content
def test_convert_bmignore_file_pattern_not_mangled_as_directory(config_home):
"""Regression: config.json must be excluded as a FILE, not as a dir content glob.
Previously `config.json` (no wildcard) became `- config.json/**`, which only
excludes contents of a *directory* named config.json, leaving the actual file
unfiltered during bisync.
"""
bmignore = get_bmignore_path()
bmignore.parent.mkdir(parents=True, exist_ok=True)
bmignore.write_text("config.json\n", encoding="utf-8")
content = convert_bmignore_to_rclone_filters().read_text(encoding="utf-8").splitlines()
# Must match the file itself
assert "- config.json" in content
# Also emit the /** form for completeness (covers any hypothetical dir of that name)
assert "- config.json/**" in content
def test_convert_bmignore_hidden_glob_covers_directory_contents(config_home):
"""Regression: `.*` must exclude hidden directory *contents*, not just hidden files.
Previously `.*` became `- .*` only. rclone globs don't cross `/`, so
`.obsidian/workspace.json` would not be matched by `- .*` alone.
"""
bmignore = get_bmignore_path()
bmignore.parent.mkdir(parents=True, exist_ok=True)
bmignore.write_text(".*\n", encoding="utf-8")
content = convert_bmignore_to_rclone_filters().read_text(encoding="utf-8").splitlines()
# Must exclude hidden files at any level
assert "- .*" in content
# Must also exclude contents of hidden directories
assert "- .*/**" in content
def test_convert_bmignore_to_rclone_filters_is_cached_when_up_to_date(config_home):
bmignore = get_bmignore_path()
bmignore.parent.mkdir(parents=True, exist_ok=True)