Skip to content
Closed
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
29 changes: 21 additions & 8 deletions src/basic_memory/ignore_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@
}


def _parse_ignore_pattern_line(raw_line: str) -> str | None:
"""Return the pattern for one gitignore-style line, or None to skip it.

Blank lines and comments (leading ``#``) are skipped. A leading backslash
escapes a pattern that itself begins with ``#`` (gitignore rule), so
``\\#*#`` yields the pattern ``#*#``.
"""
line = raw_line.strip()
if not line or line.startswith("#"):
return None
if line.startswith("\\#"):
return line[1:]
Comment on lines +78 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve leading whitespace before unescaping hashes

When a .gitignore contains \#foo, a valid pattern for the filename #foo, strip() removes the significant leading space and the new escape branch converts the remainder to #foo. Basic Memory therefore ignores/index-excludes the wrong file, while git check-ignore --no-index matches only #foo; Git's pattern format discards unescaped trailing spaces, not leading ones. Preserve leading characters when removing the line ending before recognizing the hash escape.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Addressed in replacement #1544, commit ee2f854. The parser preserves leading whitespace, decodes escaped hashes/spaces, and discards only unescaped trailing spaces. Regressions cover the reported space-plus-escaped-hash case for both loaders: 39 focused tests pass, all 22 filename expectations agree with git check-ignore, and just fast-check plus just doctor pass. Exact-head Codex gate is currently waiting on #1544.

return line


def get_bmignore_path() -> Path:
"""Get path to .bmignore file.

Expand Down Expand Up @@ -170,10 +185,9 @@ def load_bmignore_patterns() -> Set[str]:
try:
with bmignore_path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith("#"):
patterns.add(line)
pattern = _parse_ignore_pattern_line(line)
if pattern:
patterns.add(pattern)
except Exception: # pragma: no cover
# If we can't read .bmignore, fall back to defaults
return set(DEFAULT_IGNORE_PATTERNS) # pragma: no cover
Expand Down Expand Up @@ -209,10 +223,9 @@ def load_gitignore_patterns(base_path: Path, use_gitignore: bool = True) -> Set[
try:
with gitignore_file.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith("#"):
patterns.add(line)
pattern = _parse_ignore_pattern_line(line)
if pattern:
patterns.add(pattern)
except Exception:
# If we can't read .gitignore, just use default patterns
pass
Expand Down
33 changes: 33 additions & 0 deletions tests/cli/test_ignore_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from basic_memory.ignore_utils import (
DEFAULT_IGNORE_PATTERNS,
get_bmignore_path,
load_bmignore_patterns,
load_gitignore_patterns,
should_ignore_path,
filter_files,
Expand Down Expand Up @@ -330,3 +331,35 @@ def test_filter_files_with_gitignore_loading():
assert len(filtered_files) == 2
assert set(filtered_files) == set(expected_kept)
assert ignored_count == 2 # debug.log, temp_file.txt


def test_bmignore_escaped_hash_pattern_loads(tmp_path, monkeypatch):
"""Regression for #1539: an escaped hash pattern loads without the backslash."""
monkeypatch.setenv("BASIC_MEMORY_CONFIG_DIR", str(tmp_path))
(tmp_path / ".bmignore").write_text("# a comment\n\\#*#\n*~\n")

patterns = load_bmignore_patterns()

assert "#*#" in patterns
assert "\\#*#" not in patterns
assert "# a comment" not in patterns
assert should_ignore_path(tmp_path / "notes" / "#note.md#", tmp_path, patterns) is True


def test_bmignore_bare_hash_is_still_a_comment(tmp_path, monkeypatch):
"""A bare leading hash is still a comment, so it never becomes a pattern."""
monkeypatch.setenv("BASIC_MEMORY_CONFIG_DIR", str(tmp_path))
(tmp_path / ".bmignore").write_text("#*#\n")

assert "#*#" not in load_bmignore_patterns()


def test_gitignore_escaped_hash_pattern_loads(tmp_path, monkeypatch):
"""Same \\# escape fix applies to project .gitignore files."""
monkeypatch.setenv("BASIC_MEMORY_CONFIG_DIR", str(tmp_path))
(tmp_path / ".gitignore").write_text("\\#*#\n")

patterns = load_gitignore_patterns(tmp_path)

assert "#*#" in patterns
assert should_ignore_path(tmp_path / "#note.md#", tmp_path, patterns) is True