Skip to content

Commit 0aee6f3

Browse files
authored
fix(init): auto-add per-project files to .gitignore in git repos (#185) (#866)
Partially addresses #185. `mempalace init <dir>` writes `mempalace.yaml` and `entities.json` into the project root. When <dir> is a git repository, those files have no default protection and risk being committed by accident — the loudest concern in the original report. This PR adds `_ensure_mempalace_files_gitignored()` which runs at the end of cmd_init: if <dir>/.git exists, append the two filenames to .gitignore (creating it if necessary) under a clearly-marked block. The helper is conservative: - only runs when <dir>/.git is present (no-op for non-git projects) - skips entries already present (no duplicates) - preserves existing .gitignore content - handles files without trailing newlines This does NOT relocate the files to ~/.mempalace/wings/<wing>/ as the issue's 'Expected' section proposes — that's a behavioral change with miner/config implications and warrants a separate design discussion. The gitignore safeguard removes the immediate risk without breaking any existing flow. Tests: 5 cases in tests/test_init_gitignore_protection.py covering no-op, fresh creation, partial append, idempotency, and missing-newline edge case.
1 parent 6a73eb2 commit 0aee6f3

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

mempalace/cli.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,37 @@
3636
from .config import MempalaceConfig
3737

3838

39+
_MEMPALACE_PROJECT_FILES = ("mempalace.yaml", "entities.json")
40+
41+
42+
def _ensure_mempalace_files_gitignored(project_dir) -> bool:
43+
"""If project_dir is a git repo, ensure MemPalace's per-project files
44+
are listed in .gitignore so they don't get committed by accident.
45+
46+
Returns True if .gitignore was updated, False otherwise. Issue #185:
47+
`mempalace init` writes mempalace.yaml + entities.json into the
48+
project root, where they previously had no protection against being
49+
staged into git.
50+
"""
51+
from pathlib import Path
52+
53+
project_path = Path(project_dir).expanduser().resolve()
54+
if not (project_path / ".git").exists():
55+
return False
56+
gitignore = project_path / ".gitignore"
57+
existing = gitignore.read_text() if gitignore.exists() else ""
58+
existing_lines = {line.strip() for line in existing.splitlines()}
59+
missing = [p for p in _MEMPALACE_PROJECT_FILES if p not in existing_lines]
60+
if not missing:
61+
return False
62+
prefix = "" if not existing or existing.endswith("\n") else "\n"
63+
block = prefix + "\n# MemPalace per-project files (issue #185)\n" + "\n".join(missing) + "\n"
64+
with open(gitignore, "a") as f:
65+
f.write(block)
66+
print(f" Added {', '.join(missing)} to {gitignore.name}")
67+
return True
68+
69+
3970
def cmd_init(args):
4071
import json
4172
from pathlib import Path
@@ -64,6 +95,9 @@ def cmd_init(args):
6495
detect_rooms_local(project_dir=args.dir, yes=getattr(args, "yes", False))
6596
MempalaceConfig().init()
6697

98+
# Pass 3: protect git repos from accidentally committing per-project files
99+
_ensure_mempalace_files_gitignored(args.dir)
100+
67101

68102
def cmd_mine(args):
69103
palace_path = os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Regression tests for issue #185 — gitignore protection on `mempalace init`.
2+
3+
Issue #185 reports that `mempalace init <dir>` writes `mempalace.yaml` and
4+
`entities.json` into the project root, where they could be committed by
5+
accident. The fix adds `_ensure_mempalace_files_gitignored()` which appends
6+
the two filenames to `.gitignore` when `<dir>` is a git repository.
7+
"""
8+
9+
from pathlib import Path
10+
11+
from mempalace.cli import _ensure_mempalace_files_gitignored
12+
13+
14+
def _git_init(path: Path) -> None:
15+
"""Mark a directory as a git repo without invoking git itself."""
16+
(path / ".git").mkdir()
17+
18+
19+
def test_no_op_when_not_a_git_repo(tmp_path):
20+
assert _ensure_mempalace_files_gitignored(tmp_path) is False
21+
assert not (tmp_path / ".gitignore").exists()
22+
23+
24+
def test_creates_gitignore_with_both_entries(tmp_path):
25+
_git_init(tmp_path)
26+
assert _ensure_mempalace_files_gitignored(tmp_path) is True
27+
contents = (tmp_path / ".gitignore").read_text()
28+
assert "mempalace.yaml" in contents
29+
assert "entities.json" in contents
30+
assert "issue #185" in contents
31+
32+
33+
def test_appends_only_missing_entries(tmp_path):
34+
_git_init(tmp_path)
35+
(tmp_path / ".gitignore").write_text("node_modules/\nmempalace.yaml\n")
36+
assert _ensure_mempalace_files_gitignored(tmp_path) is True
37+
contents = (tmp_path / ".gitignore").read_text()
38+
# mempalace.yaml must not be duplicated
39+
assert contents.count("mempalace.yaml") == 1
40+
# entities.json was missing → must now be present
41+
assert "entities.json" in contents
42+
# original entries preserved
43+
assert "node_modules/" in contents
44+
45+
46+
def test_idempotent_when_both_already_present(tmp_path):
47+
_git_init(tmp_path)
48+
initial = "mempalace.yaml\nentities.json\n"
49+
(tmp_path / ".gitignore").write_text(initial)
50+
assert _ensure_mempalace_files_gitignored(tmp_path) is False
51+
assert (tmp_path / ".gitignore").read_text() == initial
52+
53+
54+
def test_handles_gitignore_without_trailing_newline(tmp_path):
55+
_git_init(tmp_path)
56+
(tmp_path / ".gitignore").write_text("dist") # no trailing newline
57+
assert _ensure_mempalace_files_gitignored(tmp_path) is True
58+
contents = (tmp_path / ".gitignore").read_text()
59+
# Original entry preserved on its own line, not glued to the new block
60+
assert "dist\n" in contents
61+
assert "mempalace.yaml" in contents
62+
assert "entities.json" in contents

0 commit comments

Comments
 (0)