Skip to content

Commit 5d93b72

Browse files
committed
fix(sync): preserve bmignore rclone filters
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 34830bf commit 5d93b72

4 files changed

Lines changed: 111 additions & 32 deletions

File tree

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
5656
test-sqlite-unit:
5757
name: Test SQLite Unit (${{ matrix.os }}, Python ${{ matrix.python-version }})
58-
timeout-minutes: 30
58+
timeout-minutes: 45
5959
strategy:
6060
fail-fast: false
6161
matrix:
@@ -147,7 +147,7 @@ jobs:
147147
148148
test-postgres-unit:
149149
name: Test Postgres Unit (Python ${{ matrix.python-version }})
150-
timeout-minutes: 30
150+
timeout-minutes: 60
151151
strategy:
152152
fail-fast: false
153153
matrix:

docs/cloud-cli.md

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -582,33 +582,62 @@ bm cloud logout
582582
**Default patterns:**
583583

584584
```gitignore
585+
# Hidden files and directories
586+
.*
587+
588+
# Basic Memory internals
589+
*.db
590+
*.db-shm
591+
*.db-wal
592+
config.json
593+
585594
# Version control
586-
.git/**
595+
.git
596+
.svn
587597
588598
# Python
589-
__pycache__/**
599+
__pycache__
590600
*.pyc
591-
.venv/**
592-
venv/**
601+
*.pyo
602+
*.pyd
603+
.pytest_cache
604+
.coverage
605+
*.egg-info
606+
.tox
607+
.mypy_cache
608+
.ruff_cache
609+
610+
# Virtual environments
611+
.venv
612+
venv
613+
env
614+
.env
593615
594616
# Node.js
595-
node_modules/**
617+
node_modules
596618
597-
# Basic Memory internals
598-
memory.db/**
599-
memory.db-shm/**
600-
memory.db-wal/**
601-
config.json/**
602-
watch-status.json/**
603-
.bmignore.rclone/**
619+
# Build artifacts
620+
build
621+
dist
622+
.cache
623+
624+
# IDE
625+
.idea
626+
.vscode
604627
605628
# OS files
606-
.DS_Store/**
607-
Thumbs.db/**
629+
.DS_Store
630+
Thumbs.db
631+
desktop.ini
632+
633+
# Obsidian
634+
.obsidian
608635
609-
# Environment files
610-
.env/**
611-
.env.local/**
636+
# Temporary files
637+
*.tmp
638+
*.swp
639+
*.swo
640+
*~
612641
```
613642

614643
**How it works:**
@@ -617,14 +646,19 @@ Thumbs.db/**
617646
3. Rclone uses filters during sync
618647
4. Same patterns used by all projects
619648

649+
During conversion, file patterns exclude the direct match and recursive contents.
650+
For example, `config.json` becomes both `- config.json` and `- config.json/**`,
651+
while `.*` becomes both `- .*` and `- .*/**`. Directory-only patterns keep
652+
their trailing slash, so `cache/` becomes `- cache/` and `- cache/**`.
653+
620654
**Customizing:**
621655

622656
```bash
623657
# Edit patterns
624658
code ~/.basic-memory/.bmignore
625659

626660
# Add custom patterns
627-
echo "*.tmp/**" >> ~/.basic-memory/.bmignore
661+
echo "*.tmp" >> ~/.basic-memory/.bmignore
628662

629663
# Next sync uses updated patterns
630664
bm project bisync --name research

src/basic_memory/cli/commands/cloud/bisync_commands.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ class BisyncError(Exception):
1414
pass
1515

1616

17+
def _rclone_exclude_filters(pattern: str) -> list[str]:
18+
"""Return rclone exclude filters for a gitignore-style pattern."""
19+
if pattern.endswith("/"):
20+
# Trigger: gitignore-style patterns ending in / are directory-only rules.
21+
# Why: stripping the slash would also exclude a same-named file.
22+
# Outcome: rclone keeps the directory rule and excludes recursive contents.
23+
return [f"- {pattern}", f"- {pattern}**"]
24+
25+
path_pattern = pattern.removesuffix("/**")
26+
27+
# Trigger: rclone treats a directory contents filter separately from the
28+
# directory/file path itself.
29+
# Why: files like config.json and directory markers like .obsidian must both
30+
# be excluded, along with anything below matching directories.
31+
# Outcome: every ignore pattern excludes the direct match and recursive children.
32+
return [f"- {path_pattern}", f"- {path_pattern}/**"]
33+
34+
1735
async def get_mount_info() -> TenantMountInfo:
1836
"""Get current tenant information from cloud API."""
1937
try:
@@ -78,19 +96,11 @@ def convert_bmignore_to_rclone_filters() -> Path:
7896
patterns.append(line)
7997
continue
8098

81-
# Convert gitignore pattern to rclone filter syntax
82-
# gitignore: node_modules → rclone: - node_modules/**
83-
# gitignore: *.pyc → rclone: - *.pyc
84-
if "*" in line:
85-
# Pattern already has wildcard, just add exclude prefix
86-
patterns.append(f"- {line}")
87-
else:
88-
# Directory pattern - add /** for recursive exclude
89-
patterns.append(f"- {line}/**")
99+
patterns.extend(_rclone_exclude_filters(line))
90100

91101
except Exception:
92102
# If we can't read the file, create a minimal filter
93-
patterns = ["# Error reading .bmignore, using minimal filters", "- .git/**"]
103+
patterns = ["# Error reading .bmignore, using minimal filters", "- .git", "- .git/**"]
94104

95105
# Write rclone filter file
96106
rclone_filter_path.write_text("\n".join(patterns) + "\n")

tests/cli/cloud/test_rclone_config_and_bmignore_filters.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,48 @@ def test_convert_bmignore_to_rclone_filters_creates_and_converts(config_home):
3232
# Comments/empties preserved
3333
assert "# comment" in content
3434
assert "" in content
35-
# Directory pattern becomes recursive exclude
35+
# Plain and wildcard patterns exclude direct matches and recursive contents.
36+
assert "- node_modules" in content
3637
assert "- node_modules/**" in content
37-
# Wildcard pattern becomes simple exclude
3838
assert "- *.pyc" in content
39+
assert "- *.pyc/**" in content
40+
assert "- .git" in content
3941
assert "- .git/**" in content
4042

4143

44+
def test_convert_bmignore_to_rclone_filters_excludes_files_and_hidden_directory_contents(
45+
config_home,
46+
):
47+
bmignore = get_bmignore_path()
48+
bmignore.parent.mkdir(parents=True, exist_ok=True)
49+
bmignore.write_text("config.json\n.*\nnode_modules/**\n", encoding="utf-8")
50+
51+
rclone_filter = convert_bmignore_to_rclone_filters()
52+
content = rclone_filter.read_text(encoding="utf-8").splitlines()
53+
54+
assert "- config.json" in content
55+
assert "- config.json/**" in content
56+
assert "- .*" in content
57+
assert "- .*/**" in content
58+
assert "- node_modules" in content
59+
assert "- node_modules/**" in content
60+
61+
62+
def test_convert_bmignore_to_rclone_filters_preserves_directory_only_patterns(config_home):
63+
bmignore = get_bmignore_path()
64+
bmignore.parent.mkdir(parents=True, exist_ok=True)
65+
bmignore.write_text("cache/\nconfig.json/**\n", encoding="utf-8")
66+
67+
rclone_filter = convert_bmignore_to_rclone_filters()
68+
content = rclone_filter.read_text(encoding="utf-8").splitlines()
69+
70+
assert "- cache/" in content
71+
assert "- cache/**" in content
72+
assert "- cache" not in content
73+
assert "- config.json" in content
74+
assert "- config.json/**" in content
75+
76+
4277
def test_convert_bmignore_to_rclone_filters_is_cached_when_up_to_date(config_home):
4378
bmignore = get_bmignore_path()
4479
bmignore.parent.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)