diff --git a/mempalace/cli.py b/mempalace/cli.py index f7f68d7..b06a711 100644 --- a/mempalace/cli.py +++ b/mempalace/cli.py @@ -36,6 +36,37 @@ from pathlib import Path from .config import MempalaceConfig +_MEMPALACE_PROJECT_FILES = ("mempalace.yaml", "entities.json") + + +def _ensure_mempalace_files_gitignored(project_dir) -> bool: + """If project_dir is a git repo, ensure MemPalace's per-project files + are listed in .gitignore so they don't get committed by accident. + + Returns True if .gitignore was updated, False otherwise. Issue #185: + `mempalace init` writes mempalace.yaml + entities.json into the + project root, where they previously had no protection against being + staged into git. + """ + from pathlib import Path + + project_path = Path(project_dir).expanduser().resolve() + if not (project_path / ".git").exists(): + return False + gitignore = project_path / ".gitignore" + existing = gitignore.read_text() if gitignore.exists() else "" + existing_lines = {line.strip() for line in existing.splitlines()} + missing = [p for p in _MEMPALACE_PROJECT_FILES if p not in existing_lines] + if not missing: + return False + prefix = "" if not existing or existing.endswith("\n") else "\n" + block = prefix + "\n# MemPalace per-project files (issue #185)\n" + "\n".join(missing) + "\n" + with open(gitignore, "a") as f: + f.write(block) + print(f" Added {', '.join(missing)} to {gitignore.name}") + return True + + def cmd_init(args): import json from pathlib import Path @@ -64,6 +95,9 @@ def cmd_init(args): detect_rooms_local(project_dir=args.dir, yes=getattr(args, "yes", False)) MempalaceConfig().init() + # Pass 3: protect git repos from accidentally committing per-project files + _ensure_mempalace_files_gitignored(args.dir) + def cmd_mine(args): palace_path = os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path diff --git a/tests/test_init_gitignore_protection.py b/tests/test_init_gitignore_protection.py new file mode 100644 index 0000000..ab22ea3 --- /dev/null +++ b/tests/test_init_gitignore_protection.py @@ -0,0 +1,62 @@ +"""Regression tests for issue #185 — gitignore protection on `mempalace init`. + +Issue #185 reports that `mempalace init ` writes `mempalace.yaml` and +`entities.json` into the project root, where they could be committed by +accident. The fix adds `_ensure_mempalace_files_gitignored()` which appends +the two filenames to `.gitignore` when `` is a git repository. +""" + +from pathlib import Path + +from mempalace.cli import _ensure_mempalace_files_gitignored + + +def _git_init(path: Path) -> None: + """Mark a directory as a git repo without invoking git itself.""" + (path / ".git").mkdir() + + +def test_no_op_when_not_a_git_repo(tmp_path): + assert _ensure_mempalace_files_gitignored(tmp_path) is False + assert not (tmp_path / ".gitignore").exists() + + +def test_creates_gitignore_with_both_entries(tmp_path): + _git_init(tmp_path) + assert _ensure_mempalace_files_gitignored(tmp_path) is True + contents = (tmp_path / ".gitignore").read_text() + assert "mempalace.yaml" in contents + assert "entities.json" in contents + assert "issue #185" in contents + + +def test_appends_only_missing_entries(tmp_path): + _git_init(tmp_path) + (tmp_path / ".gitignore").write_text("node_modules/\nmempalace.yaml\n") + assert _ensure_mempalace_files_gitignored(tmp_path) is True + contents = (tmp_path / ".gitignore").read_text() + # mempalace.yaml must not be duplicated + assert contents.count("mempalace.yaml") == 1 + # entities.json was missing → must now be present + assert "entities.json" in contents + # original entries preserved + assert "node_modules/" in contents + + +def test_idempotent_when_both_already_present(tmp_path): + _git_init(tmp_path) + initial = "mempalace.yaml\nentities.json\n" + (tmp_path / ".gitignore").write_text(initial) + assert _ensure_mempalace_files_gitignored(tmp_path) is False + assert (tmp_path / ".gitignore").read_text() == initial + + +def test_handles_gitignore_without_trailing_newline(tmp_path): + _git_init(tmp_path) + (tmp_path / ".gitignore").write_text("dist") # no trailing newline + assert _ensure_mempalace_files_gitignored(tmp_path) is True + contents = (tmp_path / ".gitignore").read_text() + # Original entry preserved on its own line, not glued to the new block + assert "dist\n" in contents + assert "mempalace.yaml" in contents + assert "entities.json" in contents