"""TDD tests for muse#48 Phase 1 — ``muse branch -c``/``-C`` reflog copy parity with git. ``git branch -c src dst`` copies ``src``'s reflog to ``dst`` and prepends a "copied from" entry marking the origin. Before this ticket, muse's ``-c`` wrote only the branch ref — the destination branch had no reflog at all, even when the source had a rich history. Covers BC_01–BC_07. """ 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.types import NULL_COMMIT_ID from tests.test_cmd_branch import _branch, _commit, _invoke, repo # noqa: F401 class TestCopyReflogBasic: def test_bc01_log_copied_byte_for_byte_plus_new_entry(self, repo: pathlib.Path) -> None: """BC_01: the copy is byte-for-byte identical to the source except for the new entry. Reflog files are oldest-first on disk (newest read back as @{0} only after read_reflog reverses them), so the new entry is appended after the copied bytes, not prepended before them.""" _branch(repo, "dev") _invoke(repo, ["checkout", "dev"]) (repo / "x.py").write_text("x = 1\n") _commit(repo, "-m", "dev commit") _invoke(repo, ["checkout", "main"]) src_log_path = reflog_branch_path(repo, "dev") src_bytes = src_log_path.read_bytes() result = _branch(repo, "-c", "dev", "feat/x") assert result.exit_code == 0 dst_log_path = reflog_branch_path(repo, "feat/x") dst_bytes = dst_log_path.read_bytes() # Destination log = source log (verbatim) + [new line] appended. assert dst_bytes.startswith(src_bytes) appended = dst_bytes[len(src_bytes):] assert appended.count(b"\n") == 1 def test_bc02_prepended_entry_fields(self, repo: pathlib.Path) -> None: """BC_02: the prepended entry has operation 'branch: copied from ', new_id = tip of src, old_id = null ID.""" _branch(repo, "dev") tip = _invoke(repo, ["rev-parse", "dev"]).output.strip() _branch(repo, "-c", "dev", "feat/y") entries = read_reflog(repo, branch="feat/y", limit=10) assert entries[0].operation == "branch: copied from dev" assert entries[0].new_id == tip assert entries[0].old_id == NULL_COMMIT_ID def test_bc03_reflog_cli_shows_prepended_then_source_history( self, repo: pathlib.Path ) -> None: """BC_03: ``muse reflog --branch dst`` returns the prepended entry at index 0, source history following.""" _branch(repo, "dev") _invoke(repo, ["checkout", "dev"]) (repo / "a.py").write_text("a = 1\n") _commit(repo, "-m", "dev change") _invoke(repo, ["checkout", "main"]) src_entries = read_reflog(repo, branch="dev", limit=100) _branch(repo, "-c", "dev", "backup/dev") dst_entries = read_reflog(repo, branch="backup/dev", limit=100) assert dst_entries[0].operation == "branch: copied from dev" assert [e.operation for e in dst_entries[1:]] == [e.operation for e in src_entries] def test_bc04_force_copy_replaces_destination_log(self, repo: pathlib.Path) -> None: """BC_04: force-copy (-C) replaces the destination's reflog wholesale — the previous destination log is not preserved.""" _branch(repo, "src") _branch(repo, "dst") _invoke(repo, ["checkout", "dst"]) (repo / "dst-only.py").write_text("unique = True\n") _commit(repo, "-m", "dst-only commit") _invoke(repo, ["checkout", "main"]) src_entries = read_reflog(repo, branch="src", limit=100) _branch(repo, "-C", "src", "dst") dst_entries_after = read_reflog(repo, branch="dst", limit=100) # The dst-only commit entry must be gone — wholesale replacement, # not a merge of old and new history. assert all("dst-only" not in e.operation for e in dst_entries_after) assert dst_entries_after[0].operation == "branch: copied from src" assert len(dst_entries_after) == len(src_entries) + 1 def test_bc05_source_with_no_reflog_writes_just_origin_entry( self, tmp_path: pathlib.Path ) -> None: """BC_05: if the source branch has no reflog, copy writes just the single origin entry with no prior history.""" import os saved = os.getcwd() try: os.chdir(tmp_path) from tests.cli_test_helper import CliRunner CliRunner().invoke(None, ["init"]) finally: os.chdir(saved) # "main" exists with no commits yet — no reflog file. src_log = reflog_branch_path(tmp_path, "main") assert not src_log.exists() result = _branch(tmp_path, "-c", "main", "copy-of-main") # No commits yet means no tip to copy — branch -c with an empty # source produces an empty destination branch and no reflog either # (there is no tip to record a "copied from" entry about). assert result.exit_code == 0 dst_entries = read_reflog(tmp_path, branch="copy-of-main", limit=10) assert dst_entries == [] def test_bc05b_source_with_reflog_but_dest_from_scratch(self, repo: pathlib.Path) -> None: """Companion to BC_05: a source *with* a reflog but freshly copied produces exactly source-entries + 1, with no leftover state.""" _branch(repo, "dev") src_entries = read_reflog(repo, branch="dev", limit=100) assert len(src_entries) == 1 # just "created from main" _branch(repo, "-c", "dev", "fresh/copy") dst_entries = read_reflog(repo, branch="fresh/copy", limit=100) assert len(dst_entries) == len(src_entries) + 1 def test_bc06_ref_written_before_reflog_copy_is_safe_if_interrupted( self, repo: pathlib.Path ) -> None: """BC_06: if interrupted after the ref write but before the log write, the branch is still valid (just missing the log) — simulated by writing only the ref and confirming read_reflog degrades gracefully rather than raising.""" from muse.core.refs import write_branch_ref, get_head_commit_id tip = get_head_commit_id(repo, "main") write_branch_ref(repo, "half-copied", tip) # No reflog written yet — simulates an interruption between steps. entries = read_reflog(repo, branch="half-copied", limit=10) assert entries == [] def test_bc07_integration_five_entries_copy_yields_six(self, repo: pathlib.Path) -> None: """BC_07: a branch with N reflog entries -> muse branch -c -> the destination shows N+1 entries (source history + the copy marker).""" _branch(repo, "dev") _invoke(repo, ["checkout", "dev"]) for i in range(4): (repo / f"f{i}.py").write_text(f"v = {i}\n") _commit(repo, "-m", f"change {i}") _invoke(repo, ["checkout", "main"]) src_entries = read_reflog(repo, branch="dev", limit=100) n = len(src_entries) result = _branch(repo, "-c", "dev", "backup/dev") assert result.exit_code == 0 dst_entries = read_reflog(repo, branch="backup/dev", limit=100) assert len(dst_entries) == n + 1