gabriel / muse public

test_cmd_branch_rename_reflog.py file-level

at sha256:f · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:7 fix: muse mist push crashed unconditionally with AttributeError (musehu… · gabriel · Oct 22, 2026
1 """TDD tests for muse#48 Phase 2 — ``muse branch -m``/``-M`` reflog move parity with git.
2
3 ``git branch -m old new`` moves the reflog file from ``old`` to ``new``,
4 preserving full history under the new name, and (if the renamed branch is
5 HEAD) records the rename in the HEAD log too. Before this ticket, muse's
6 ``-m`` moved only the ref — the old log was orphaned at the old path and the
7 new name had no log at all.
8
9 Covers BC_08–BC_15.
10 """
11
12 from __future__ import annotations
13
14 import pathlib
15
16 import pytest
17
18 from muse.core.paths import reflog_branch_path
19 from muse.core.reflog import read_reflog
20 from muse.core.refs import read_current_branch
21 from tests.test_cmd_branch import _branch, _commit, _invoke, repo # noqa: F401
22
23
24 class TestRenameReflogBasic:
25 def test_bc08_log_moved_old_path_removed(self, repo: pathlib.Path) -> None:
26 """BC_08: rename moves the reflog file; the old path is removed."""
27 _branch(repo, "feat/long-name")
28 old_path = reflog_branch_path(repo, "feat/long-name")
29 assert old_path.is_file()
30
31 result = _branch(repo, "-m", "feat/long-name", "feat/short")
32 assert result.exit_code == 0
33
34 assert not old_path.exists()
35 assert reflog_branch_path(repo, "feat/short").is_file()
36
37 def test_bc09_appended_entry_old_id_equals_new_id_equals_tip(
38 self, repo: pathlib.Path
39 ) -> None:
40 """BC_09: the new log has an appended entry with operation 'branch:
41 renamed from <old>', old_id == new_id == branch tip (the commit
42 doesn't change, only the name)."""
43 _branch(repo, "old-name")
44 tip = _invoke(repo, ["rev-parse", "old-name"]).output.strip()
45
46 _branch(repo, "-m", "old-name", "new-name")
47
48 entries = read_reflog(repo, branch="new-name", limit=100)
49 assert entries[0].operation == "branch: renamed from old-name"
50 assert entries[0].old_id == tip
51 assert entries[0].new_id == tip
52
53 def test_bc10_prior_entries_preserved_plus_rename_entry(
54 self, repo: pathlib.Path
55 ) -> None:
56 """BC_10: after rename, the new log shows all prior entries (content
57 unchanged) plus the rename entry at the newest position (index 0)."""
58 _branch(repo, "feat/x")
59 _invoke(repo, ["checkout", "feat/x"])
60 (repo / "y.py").write_text("y = 1\n")
61 _commit(repo, "-m", "feat change")
62 _invoke(repo, ["checkout", "main"])
63
64 src_entries = read_reflog(repo, branch="feat/x", limit=100)
65
66 _branch(repo, "-m", "feat/x", "feat/y")
67 dst_entries = read_reflog(repo, branch="feat/y", limit=100)
68
69 assert dst_entries[0].operation == "branch: renamed from feat/x"
70 assert [e.operation for e in dst_entries[1:]] == [e.operation for e in src_entries]
71
72 def test_bc11_renaming_checked_out_branch_writes_head_log_entry(
73 self, repo: pathlib.Path
74 ) -> None:
75 """BC_11: when renaming the currently checked-out branch, a 'branch:
76 renamed from <old> to <new>' entry is also written to the HEAD log."""
77 _branch(repo, "current-branch")
78 _invoke(repo, ["checkout", "current-branch"])
79
80 head_entries_before = read_reflog(repo, branch=None, limit=100)
81
82 _branch(repo, "-m", "current-branch", "renamed-branch")
83
84 assert read_current_branch(repo) == "renamed-branch"
85 head_entries_after = read_reflog(repo, branch=None, limit=100)
86 assert len(head_entries_after) == len(head_entries_before) + 1
87 assert head_entries_after[0].operation == (
88 "branch: renamed from current-branch to renamed-branch"
89 )
90
91 _invoke(repo, ["checkout", "main"])
92
93 def test_bc11b_renaming_non_checked_out_branch_does_not_touch_head_log(
94 self, repo: pathlib.Path
95 ) -> None:
96 """Companion to BC_11: renaming a branch that is NOT checked out
97 must not write anything to the HEAD log."""
98 _branch(repo, "not-current")
99 head_entries_before = read_reflog(repo, branch=None, limit=100)
100
101 _branch(repo, "-m", "not-current", "still-not-current")
102
103 head_entries_after = read_reflog(repo, branch=None, limit=100)
104 assert len(head_entries_after) == len(head_entries_before)
105
106 def test_bc12_force_rename_same_reflog_semantics(self, repo: pathlib.Path) -> None:
107 """BC_12: force-rename (-M) carries identical reflog semantics; the
108 destination's prior log (if any) is discarded."""
109 _branch(repo, "a")
110 _branch(repo, "b")
111 _invoke(repo, ["checkout", "b"])
112 (repo / "b-only.py").write_text("unique = True\n")
113 _commit(repo, "-m", "b-only commit")
114 _invoke(repo, ["checkout", "main"])
115
116 a_entries = read_reflog(repo, branch="a", limit=100)
117
118 result = _branch(repo, "-M", "a", "b")
119 assert result.exit_code == 0
120
121 b_entries_after = read_reflog(repo, branch="b", limit=100)
122 assert all("b-only" not in e.operation for e in b_entries_after)
123 assert b_entries_after[0].operation == "branch: renamed from a"
124 assert len(b_entries_after) == len(a_entries) + 1
125
126 def test_bc13_source_preserved_if_destination_write_fails(
127 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
128 ) -> None:
129 """BC_13: if the destination write fails, the source log is
130 preserved (move is atomic: write dest fully, then unlink source)."""
131 import muse.core.reflog as reflog_mod
132
133 _branch(repo, "will-fail")
134 src_path = reflog_branch_path(repo, "will-fail")
135 assert src_path.is_file()
136 original_bytes = src_path.read_bytes()
137
138 def _boom(*args: object, **kwargs: object) -> None:
139 raise OSError("simulated disk failure")
140
141 monkeypatch.setattr(reflog_mod.os, "replace", _boom)
142
143 with pytest.raises(OSError):
144 reflog_mod.rename_branch_reflog(
145 repo, "will-fail", "renamed-target", "sha256:" + "a" * 64, "user",
146 "branch: renamed from will-fail",
147 )
148
149 # Source untouched — os.replace never completed.
150 assert src_path.is_file()
151 assert src_path.read_bytes() == original_bytes
152
153 def test_bc14_parent_directories_created_for_destination(
154 self, repo: pathlib.Path
155 ) -> None:
156 """BC_14: parent directories for the destination path are created if
157 needed (e.g. renaming to a deeply-nested namespace)."""
158 _branch(repo, "dev")
159 result = _branch(repo, "-m", "dev", "archive/old/dev")
160 assert result.exit_code == 0
161 assert reflog_branch_path(repo, "archive/old/dev").is_file()
162
163 def test_bc15_integration_five_entries_then_checked_out_rename(
164 self, repo: pathlib.Path
165 ) -> None:
166 """BC_15: rename a 5-entry branch -> reflog shows 6 entries -> rename
167 the currently checked-out branch -> HEAD log gains one entry."""
168 _branch(repo, "feat/thing")
169 _invoke(repo, ["checkout", "feat/thing"])
170 for i in range(3):
171 (repo / f"g{i}.py").write_text(f"v = {i}\n")
172 _commit(repo, "-m", f"change {i}")
173 # feat/thing now has: create + checkout-into + 3 commits = 5 entries.
174 src_entries = read_reflog(repo, branch="feat/thing", limit=100)
175 n = len(src_entries)
176
177 head_entries_before = read_reflog(repo, branch=None, limit=100)
178
179 result = _branch(repo, "-m", "feat/thing", "feat/renamed")
180 assert result.exit_code == 0
181
182 dst_entries = read_reflog(repo, branch="feat/renamed", limit=100)
183 assert len(dst_entries) == n + 1
184
185 head_entries_after = read_reflog(repo, branch=None, limit=100)
186 assert len(head_entries_after) == len(head_entries_before) + 1
187
188 _invoke(repo, ["checkout", "main"])