"""TDD tests for muse#48 Phase 2 — ``muse branch -m``/``-M`` reflog move parity with git. ``git branch -m old new`` moves the reflog file from ``old`` to ``new``, preserving full history under the new name, and (if the renamed branch is HEAD) records the rename in the HEAD log too. Before this ticket, muse's ``-m`` moved only the ref — the old log was orphaned at the old path and the new name had no log at all. Covers BC_08–BC_15. """ from __future__ import annotations import pathlib import pytest from muse.core.paths import reflog_branch_path from muse.core.reflog import read_reflog from muse.core.refs import read_current_branch from tests.test_cmd_branch import _branch, _commit, _invoke, repo # noqa: F401 class TestRenameReflogBasic: def test_bc08_log_moved_old_path_removed(self, repo: pathlib.Path) -> None: """BC_08: rename moves the reflog file; the old path is removed.""" _branch(repo, "feat/long-name") old_path = reflog_branch_path(repo, "feat/long-name") assert old_path.is_file() result = _branch(repo, "-m", "feat/long-name", "feat/short") assert result.exit_code == 0 assert not old_path.exists() assert reflog_branch_path(repo, "feat/short").is_file() def test_bc09_appended_entry_old_id_equals_new_id_equals_tip( self, repo: pathlib.Path ) -> None: """BC_09: the new log has an appended entry with operation 'branch: renamed from ', old_id == new_id == branch tip (the commit doesn't change, only the name).""" _branch(repo, "old-name") tip = _invoke(repo, ["rev-parse", "old-name"]).output.strip() _branch(repo, "-m", "old-name", "new-name") entries = read_reflog(repo, branch="new-name", limit=100) assert entries[0].operation == "branch: renamed from old-name" assert entries[0].old_id == tip assert entries[0].new_id == tip def test_bc10_prior_entries_preserved_plus_rename_entry( self, repo: pathlib.Path ) -> None: """BC_10: after rename, the new log shows all prior entries (content unchanged) plus the rename entry at the newest position (index 0).""" _branch(repo, "feat/x") _invoke(repo, ["checkout", "feat/x"]) (repo / "y.py").write_text("y = 1\n") _commit(repo, "-m", "feat change") _invoke(repo, ["checkout", "main"]) src_entries = read_reflog(repo, branch="feat/x", limit=100) _branch(repo, "-m", "feat/x", "feat/y") dst_entries = read_reflog(repo, branch="feat/y", limit=100) assert dst_entries[0].operation == "branch: renamed from feat/x" assert [e.operation for e in dst_entries[1:]] == [e.operation for e in src_entries] def test_bc11_renaming_checked_out_branch_writes_head_log_entry( self, repo: pathlib.Path ) -> None: """BC_11: when renaming the currently checked-out branch, a 'branch: renamed from to ' entry is also written to the HEAD log.""" _branch(repo, "current-branch") _invoke(repo, ["checkout", "current-branch"]) head_entries_before = read_reflog(repo, branch=None, limit=100) _branch(repo, "-m", "current-branch", "renamed-branch") assert read_current_branch(repo) == "renamed-branch" head_entries_after = read_reflog(repo, branch=None, limit=100) assert len(head_entries_after) == len(head_entries_before) + 1 assert head_entries_after[0].operation == ( "branch: renamed from current-branch to renamed-branch" ) _invoke(repo, ["checkout", "main"]) def test_bc11b_renaming_non_checked_out_branch_does_not_touch_head_log( self, repo: pathlib.Path ) -> None: """Companion to BC_11: renaming a branch that is NOT checked out must not write anything to the HEAD log.""" _branch(repo, "not-current") head_entries_before = read_reflog(repo, branch=None, limit=100) _branch(repo, "-m", "not-current", "still-not-current") head_entries_after = read_reflog(repo, branch=None, limit=100) assert len(head_entries_after) == len(head_entries_before) def test_bc12_force_rename_same_reflog_semantics(self, repo: pathlib.Path) -> None: """BC_12: force-rename (-M) carries identical reflog semantics; the destination's prior log (if any) is discarded.""" _branch(repo, "a") _branch(repo, "b") _invoke(repo, ["checkout", "b"]) (repo / "b-only.py").write_text("unique = True\n") _commit(repo, "-m", "b-only commit") _invoke(repo, ["checkout", "main"]) a_entries = read_reflog(repo, branch="a", limit=100) result = _branch(repo, "-M", "a", "b") assert result.exit_code == 0 b_entries_after = read_reflog(repo, branch="b", limit=100) assert all("b-only" not in e.operation for e in b_entries_after) assert b_entries_after[0].operation == "branch: renamed from a" assert len(b_entries_after) == len(a_entries) + 1 def test_bc13_source_preserved_if_destination_write_fails( self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """BC_13: if the destination write fails, the source log is preserved (move is atomic: write dest fully, then unlink source).""" import muse.core.reflog as reflog_mod _branch(repo, "will-fail") src_path = reflog_branch_path(repo, "will-fail") assert src_path.is_file() original_bytes = src_path.read_bytes() def _boom(*args: object, **kwargs: object) -> None: raise OSError("simulated disk failure") monkeypatch.setattr(reflog_mod.os, "replace", _boom) with pytest.raises(OSError): reflog_mod.rename_branch_reflog( repo, "will-fail", "renamed-target", "sha256:" + "a" * 64, "user", "branch: renamed from will-fail", ) # Source untouched — os.replace never completed. assert src_path.is_file() assert src_path.read_bytes() == original_bytes def test_bc14_parent_directories_created_for_destination( self, repo: pathlib.Path ) -> None: """BC_14: parent directories for the destination path are created if needed (e.g. renaming to a deeply-nested namespace).""" _branch(repo, "dev") result = _branch(repo, "-m", "dev", "archive/old/dev") assert result.exit_code == 0 assert reflog_branch_path(repo, "archive/old/dev").is_file() def test_bc15_integration_five_entries_then_checked_out_rename( self, repo: pathlib.Path ) -> None: """BC_15: rename a 5-entry branch -> reflog shows 6 entries -> rename the currently checked-out branch -> HEAD log gains one entry.""" _branch(repo, "feat/thing") _invoke(repo, ["checkout", "feat/thing"]) for i in range(3): (repo / f"g{i}.py").write_text(f"v = {i}\n") _commit(repo, "-m", f"change {i}") # feat/thing now has: create + checkout-into + 3 commits = 5 entries. src_entries = read_reflog(repo, branch="feat/thing", limit=100) n = len(src_entries) head_entries_before = read_reflog(repo, branch=None, limit=100) result = _branch(repo, "-m", "feat/thing", "feat/renamed") assert result.exit_code == 0 dst_entries = read_reflog(repo, branch="feat/renamed", limit=100) assert len(dst_entries) == n + 1 head_entries_after = read_reflog(repo, branch=None, limit=100) assert len(head_entries_after) == len(head_entries_before) + 1 _invoke(repo, ["checkout", "main"])