"""muse#54 item 5 -- reflog entries for `muse tag add` / `muse release add`. `muse tag add` and `muse release add` move refs/tags/*-equivalent pointers with no reflog trace at all before this change. Each gets its own reflog namespace under .muse/logs/refs/tags/ (separate from branches), viewable via `muse reflog --tag `. """ from __future__ import annotations import datetime import json import pathlib from tests.cli_test_helper import CliRunner from muse.core.types import fake_id, Manifest from muse.core.paths import ref_path, muse_dir cli = None runner = CliRunner() def _env(root: pathlib.Path) -> Manifest: return {"MUSE_REPO_ROOT": str(root)} def _run(root: pathlib.Path, *args: str): return runner.invoke(cli, list(args), env=_env(root)) def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]: dot_muse = muse_dir(tmp_path) dot_muse.mkdir() repo_id = fake_id("repo") (dot_muse / "repo.json").write_text(json.dumps({ "repo_id": repo_id, "domain": "code", "default_branch": "main", "created_at": "2025-01-01T00:00:00+00:00", }), encoding="utf-8") (dot_muse / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8") (dot_muse / "refs" / "heads").mkdir(parents=True) (dot_muse / "snapshots").mkdir() (dot_muse / "commits").mkdir() (dot_muse / "objects").mkdir() return tmp_path, repo_id def _make_commit(root: pathlib.Path, message: str = "feat: add") -> str: from muse.core.ids import hash_snapshot, hash_commit from muse.core.commits import CommitRecord, write_commit from muse.core.snapshots import SnapshotRecord, write_snapshot ref_file = ref_path(root, "main") raw_parent = ref_file.read_text().strip() if ref_file.exists() else "" parent_id: str | None = raw_parent if raw_parent else None manifest: Manifest = {} snap_id = hash_snapshot(manifest) write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) now = datetime.datetime.now(datetime.timezone.utc) parent_ids: list[str] = [parent_id] if parent_id else [] commit_id = hash_commit( parent_ids=parent_ids, snapshot_id=snap_id, message=message, committed_at_iso=now.isoformat(), author="gabriel", ) write_commit(root, CommitRecord( commit_id=commit_id, branch="main", snapshot_id=snap_id, message=message, committed_at=now, parent_commit_id=parent_id, author="gabriel", )) ref_file.write_text(commit_id, encoding="utf-8") return commit_id class TestTagAddReflog: def test_new_tag_logs_creation_entry(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _make_commit(root) result = _run(root, "tag", "add", "v1.0.0") assert result.exit_code == 0, result.output from muse.core.reflog import read_tag_reflog entries = read_tag_reflog(root, "v1.0.0") assert len(entries) == 1 assert entries[0].new_id == commit_id assert entries[0].old_id == "0" * 64 assert "tag: tagged v1.0.0" in entries[0].operation def test_repointed_tag_logs_old_and_new_commit(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) first_commit = _make_commit(root, message="first") _run(root, "tag", "add", "v1.0.0") second_commit = _make_commit(root, message="second") result = _run(root, "tag", "add", "v1.0.0", "--json") assert result.exit_code == 0, result.output from muse.core.reflog import read_tag_reflog entries = read_tag_reflog(root, "v1.0.0") assert len(entries) == 2 assert entries[0].old_id == first_commit assert entries[0].new_id == second_commit def test_does_not_appear_in_head_reflog(self, tmp_path: pathlib.Path) -> None: """Tagging doesn't move HEAD -- it must not pollute the HEAD/branch reflog that `muse reflog` shows by default.""" root, repo_id = _init_repo(tmp_path) _make_commit(root) from muse.core.reflog import read_reflog before = len(read_reflog(root)) _run(root, "tag", "add", "v1.0.0") after = len(read_reflog(root)) assert after == before class TestReleaseAddReflog: def test_new_release_logs_creation_entry(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _make_commit(root) result = _run(root, "release", "add", "v1.0.0") assert result.exit_code == 0, result.output from muse.core.reflog import read_tag_reflog entries = read_tag_reflog(root, "v1.0.0") assert len(entries) == 1 assert entries[0].new_id == commit_id assert "release: created v1.0.0" in entries[0].operation def test_draft_release_notes_draft_in_operation(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) _make_commit(root) _run(root, "release", "add", "v1.0.0-beta", "--draft") from muse.core.reflog import read_tag_reflog entries = read_tag_reflog(root, "v1.0.0-beta") assert "(draft)" in entries[0].operation class TestReflogTagFlag: def test_reflog_tag_shows_tag_history(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) commit_id = _make_commit(root) _run(root, "tag", "add", "v1.0.0") result = _run(root, "reflog", "--tag", "v1.0.0", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["ref"] == "refs/tags/v1.0.0" assert data["total"] == 1 assert data["entries"][0]["new_id"] == commit_id def test_reflog_tag_and_branch_mutually_exclusive(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) _make_commit(root) result = _run(root, "reflog", "--tag", "v1.0.0", "--branch", "main", "--json") assert result.exit_code == 1 def test_reflog_tag_empty_when_tag_never_created(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) _make_commit(root) result = _run(root, "reflog", "--tag", "v9.9.9", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["total"] == 0