gabriel / muse public
test_reflog_ref_resolution.py python
609 lines 22.7 KB
Raw
sha256:c6483b12ab17f2b6b1de0abb0b7f40fe0c53456c1e503f00136d903ed8d48e0e feat(checkout): accept @{N} reflog refs for detached-HEAD r… Sonnet 5 patch 2 days ago
1 """Phase 5 — RL_21–RL_27: @{N} ref resolution.
2
3 TDD: tests written before implementation.
4
5 Coverage tiers
6 --------------
7 RL_21 muse rev-parse @{0} --json resolves to the commit_id at HEAD reflog index 0
8 RL_22 muse reset @{1} --json resets HEAD to reflog index 1; muse read confirms it
9 RL_23 muse read @{2} --json reads the commit at HEAD reflog index 2
10 RL_24 dev@{0} resolves to the dev branch reflog's newest entry commit ID
11 RL_25 Out-of-range index exits USER_ERROR with valid-range message
12 RL_26 muse diff @{1} HEAD --json shows the diff between reflog state 1 and HEAD
13 RL_27 End-to-end recovery: commit A → commit B → muse reset @{1} → back at A
14 """
15 from __future__ import annotations
16
17 import datetime
18 import json
19 import pathlib
20 import time
21
22 import pytest
23
24 from muse.core.paths import muse_dir, logs_dir
25 from muse.core.types import NULL_COMMIT_ID
26 from tests.cli_test_helper import CliRunner
27
28 runner = CliRunner()
29 cli = None
30
31 # ---------------------------------------------------------------------------
32 # Helpers
33 # ---------------------------------------------------------------------------
34
35 def _env(root: pathlib.Path) -> dict[str, str]:
36 return {"MUSE_REPO_ROOT": str(root)}
37
38
39 def _head_log(root: pathlib.Path) -> pathlib.Path:
40 return logs_dir(root) / "HEAD"
41
42
43 def _branch_log(root: pathlib.Path, branch: str) -> pathlib.Path:
44 return logs_dir(root) / "refs" / "heads" / branch
45
46
47 def _write_reflog_entry(
48 log_path: pathlib.Path,
49 old_id: str,
50 new_id: str,
51 operation: str,
52 ) -> None:
53 """Append one raw reflog line (oldest-first order, newest appended last)."""
54 ts = int(time.time())
55 line = f"{old_id} {new_id} user {ts} +0000\t{operation}\n"
56 log_path.parent.mkdir(parents=True, exist_ok=True)
57 with log_path.open("a", encoding="utf-8") as fh:
58 fh.write(line)
59
60
61 def _make_commit(
62 root: pathlib.Path,
63 parent_id: str | None,
64 branch: str,
65 message: str,
66 filename: str,
67 content: bytes,
68 ) -> str:
69 """Write a commit to the object store and return its commit_id.
70
71 Does NOT advance the branch ref — callers do that.
72 """
73 from muse.core.commits import CommitRecord, write_commit
74 from muse.core.ids import hash_commit, hash_snapshot
75 from muse.core.object_store import write_object
76 from muse.core.snapshots import SnapshotRecord, write_snapshot
77 from muse.core.types import blob_id
78
79 oid = blob_id(content)
80 write_object(root, oid, content)
81 snap_manifest = {filename: oid}
82 snap_id = hash_snapshot(snap_manifest)
83 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=snap_manifest))
84 ts = datetime.datetime.now(tz=datetime.timezone.utc)
85 parent_ids = [parent_id] if parent_id else []
86 cid = hash_commit(
87 parent_ids=parent_ids,
88 snapshot_id=snap_id,
89 message=message,
90 committed_at_iso=ts.isoformat(),
91 )
92 write_commit(root, CommitRecord(
93 commit_id=cid, branch=branch, snapshot_id=snap_id,
94 message=message, committed_at=ts,
95 parent_commit_id=parent_id,
96 ))
97 return cid
98
99
100 def _set_branch(root: pathlib.Path, branch: str, commit_id: str) -> None:
101 """Unconditionally advance a branch ref to commit_id."""
102 (muse_dir(root) / "refs" / "heads" / branch).write_text(commit_id)
103
104
105 def _make_base_repo(tmp_path: pathlib.Path, branch: str = "main") -> pathlib.Path:
106 """Create the minimal directory structure for a Muse repo (no commits)."""
107 from muse._version import __version__
108
109 dot = muse_dir(tmp_path)
110 for sub in ("refs/heads", "objects/sha256", "commits", "snapshots",
111 "logs/refs/heads", "logs"):
112 (dot / sub).mkdir(parents=True, exist_ok=True)
113 (dot / "repo.json").write_text(
114 json.dumps({"repo_id": "test-repo", "schema_version": __version__, "domain": "code"})
115 )
116 (dot / "HEAD").write_text(f"ref: refs/heads/{branch}\n")
117 (dot / "config.toml").write_text("")
118 return tmp_path
119
120
121 def _setup_repo_with_history(
122 tmp_path: pathlib.Path,
123 n_commits: int = 3,
124 branch: str = "main",
125 ) -> tuple[pathlib.Path, list[str]]:
126 """Create a repo with n_commits and matching reflog entries.
127
128 Returns (root, commit_ids) where commit_ids[0] is the oldest and
129 commit_ids[-1] is the current HEAD (newest).
130
131 Reflog is written oldest-first so:
132 index 0 (newest) = commit_ids[-1]
133 index 1 = commit_ids[-2]
134 index 2 = commit_ids[-3]
135 """
136 root = _make_base_repo(tmp_path, branch=branch)
137 commit_ids: list[str] = []
138 prev: str | None = None
139
140 for i in range(n_commits):
141 cid = _make_commit(
142 root, prev, branch,
143 message=f"commit-{i}",
144 filename=f"file{i}.txt",
145 content=f"content-{i}\n".encode(),
146 )
147 commit_ids.append(cid)
148 old = prev if prev is not None else NULL_COMMIT_ID
149 _write_reflog_entry(_head_log(root), old, cid, f"commit: commit-{i}")
150 _write_reflog_entry(_branch_log(root, branch), old, cid, f"commit: commit-{i}")
151 prev = cid
152
153 # Set branch to the newest commit.
154 _set_branch(root, branch, commit_ids[-1])
155 return root, commit_ids
156
157
158 # ---------------------------------------------------------------------------
159 # RL_21 — muse rev-parse @{0} resolves to commit_id at HEAD reflog index 0
160 # ---------------------------------------------------------------------------
161
162 class TestRevParseAtN:
163 """RL_21: rev-parse @{N} resolves via the HEAD reflog."""
164
165 def test_at_zero_resolves_to_newest_commit(self, tmp_path: pathlib.Path) -> None:
166 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
167 newest = cids[-1]
168
169 r = runner.invoke(cli, ["rev-parse", "@{0}", "--json"], env=_env(root))
170
171 assert r.exit_code == 0, r.output
172 data = json.loads(r.output)
173 assert data["commit_id"] == newest
174
175 def test_at_one_resolves_to_second_newest(self, tmp_path: pathlib.Path) -> None:
176 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
177
178 r = runner.invoke(cli, ["rev-parse", "@{1}", "--json"], env=_env(root))
179
180 assert r.exit_code == 0, r.output
181 data = json.loads(r.output)
182 assert data["commit_id"] == cids[-2]
183
184 def test_at_zero_text_mode_prints_commit_id(self, tmp_path: pathlib.Path) -> None:
185 root, cids = _setup_repo_with_history(tmp_path, n_commits=2)
186
187 r = runner.invoke(cli, ["rev-parse", "@{0}"], env=_env(root))
188
189 assert r.exit_code == 0, r.output
190 assert r.output.strip() == cids[-1]
191
192 def test_ref_field_echoes_at_spec(self, tmp_path: pathlib.Path) -> None:
193 root, cids = _setup_repo_with_history(tmp_path, n_commits=2)
194
195 r = runner.invoke(cli, ["rev-parse", "@{0}", "--json"], env=_env(root))
196
197 data = json.loads(r.output)
198 assert data["ref"] == "@{0}"
199
200
201 # ---------------------------------------------------------------------------
202 # RL_22 — muse reset @{1} resets HEAD to reflog index 1
203 # ---------------------------------------------------------------------------
204
205 class TestResetAtN:
206 """RL_22: reset @{N} moves the branch pointer to the commit at index N."""
207
208 def test_reset_at_one_moves_to_second_commit(self, tmp_path: pathlib.Path) -> None:
209 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
210
211 r = runner.invoke(cli, ["reset", "@{1}", "--json"], env=_env(root))
212
213 assert r.exit_code == 0, r.output
214 data = json.loads(r.output)
215 assert data["new_commit_id"] == cids[-2]
216
217 def test_reset_at_two_moves_to_oldest_commit(self, tmp_path: pathlib.Path) -> None:
218 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
219
220 runner.invoke(cli, ["reset", "@{2}"], env=_env(root))
221
222 r = runner.invoke(cli, ["rev-parse", "HEAD", "--json"], env=_env(root))
223 data = json.loads(r.output)
224 assert data["commit_id"] == cids[0]
225
226 def test_branch_ref_advances_after_reset(self, tmp_path: pathlib.Path) -> None:
227 """After muse reset @{1}, the branch ref file must contain the target."""
228 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
229 target = cids[-2]
230
231 runner.invoke(cli, ["reset", "@{1}"], env=_env(root))
232
233 from muse.core.refs import read_ref
234 from muse.core.paths import ref_path
235 actual = read_ref(ref_path(root, "main"))
236 assert actual == target
237
238
239 # ---------------------------------------------------------------------------
240 # muse checkout @{N} — detached-HEAD recovery (ticket's "Commands that must
241 # accept @{N}" table; no standalone RL_* ID, but a named Phase 5 goal).
242 # ---------------------------------------------------------------------------
243
244 class TestCheckoutAtN:
245 """muse checkout @{N} detaches HEAD at the reflog-resolved commit."""
246
247 def test_checkout_at_one_detaches_at_second_commit(
248 self, tmp_path: pathlib.Path
249 ) -> None:
250 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
251
252 r = runner.invoke(cli, ["checkout", "@{1}", "--force", "--json"], env=_env(root))
253
254 assert r.exit_code == 0, r.output
255 data = json.loads(r.output)
256 assert data["action"] == "detached"
257 assert data["branch"] is None
258 assert data["commit_id"] == cids[-2]
259
260 def test_checkout_at_n_does_not_raise_invalid_branch_name(
261 self, tmp_path: pathlib.Path
262 ) -> None:
263 """Regression: @{N} contains '{'/'}' — forbidden branch-name
264 characters — so it must never reach validate_branch_name."""
265 root, _cids = _setup_repo_with_history(tmp_path, n_commits=2)
266
267 r = runner.invoke(cli, ["checkout", "@{0}", "--force"], env=_env(root))
268
269 assert r.exit_code == 0, r.output
270 assert "Invalid branch name" not in r.output
271
272 def test_checkout_branch_at_n_detaches_at_branch_reflog_commit(
273 self, tmp_path: pathlib.Path
274 ) -> None:
275 root, cids = _setup_repo_with_history(tmp_path, n_commits=3, branch="main")
276
277 r = runner.invoke(cli, ["checkout", "main@{2}", "--force", "--json"], env=_env(root))
278
279 assert r.exit_code == 0, r.output
280 data = json.loads(r.output)
281 assert data["action"] == "detached"
282 assert data["commit_id"] == cids[0]
283
284 def test_checkout_at_n_out_of_range_exits_user_error(
285 self, tmp_path: pathlib.Path
286 ) -> None:
287 root, _cids = _setup_repo_with_history(tmp_path, n_commits=2)
288
289 r = runner.invoke(cli, ["checkout", "@{99}"], env=_env(root))
290
291 assert r.exit_code != 0
292
293
294 # ---------------------------------------------------------------------------
295 # RL_23 — muse read @{2} reads the commit at HEAD reflog index 2
296 # ---------------------------------------------------------------------------
297
298 class TestReadAtN:
299 """RL_23: muse read @{N} reads the commit at reflog index N."""
300
301 def test_read_at_zero_shows_newest_commit(self, tmp_path: pathlib.Path) -> None:
302 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
303
304 r = runner.invoke(cli, ["read", "@{0}", "--json"], env=_env(root))
305
306 assert r.exit_code == 0, r.output
307 data = json.loads(r.output)
308 assert data["commit_id"] == cids[-1]
309
310 def test_read_at_two_shows_oldest_commit(self, tmp_path: pathlib.Path) -> None:
311 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
312
313 r = runner.invoke(cli, ["read", "@{2}", "--json"], env=_env(root))
314
315 assert r.exit_code == 0, r.output
316 data = json.loads(r.output)
317 assert data["commit_id"] == cids[0]
318
319 def test_read_at_n_shows_correct_message(self, tmp_path: pathlib.Path) -> None:
320 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
321
322 r = runner.invoke(cli, ["read", "@{1}", "--json"], env=_env(root))
323
324 data = json.loads(r.output)
325 assert data["message"] == "commit-1"
326
327
328 # ---------------------------------------------------------------------------
329 # RL_24 — dev@{0} resolves to the dev branch reflog's newest commit ID
330 # ---------------------------------------------------------------------------
331
332 class TestBranchAtN:
333 """RL_24: <branch>@{N} resolves via the named branch reflog."""
334
335 def test_branch_at_zero_resolves_to_newest(self, tmp_path: pathlib.Path) -> None:
336 root, cids = _setup_repo_with_history(tmp_path, n_commits=3, branch="dev")
337
338 r = runner.invoke(cli, ["rev-parse", "dev@{0}", "--json"], env=_env(root))
339
340 assert r.exit_code == 0, r.output
341 data = json.loads(r.output)
342 assert data["commit_id"] == cids[-1]
343
344 def test_branch_at_one_resolves_via_branch_log(self, tmp_path: pathlib.Path) -> None:
345 root, cids = _setup_repo_with_history(tmp_path, n_commits=3, branch="dev")
346
347 r = runner.invoke(cli, ["rev-parse", "dev@{1}", "--json"], env=_env(root))
348
349 assert r.exit_code == 0, r.output
350 data = json.loads(r.output)
351 assert data["commit_id"] == cids[-2]
352
353 def test_refs_heads_prefix_form_accepted(self, tmp_path: pathlib.Path) -> None:
354 """refs/heads/dev@{0} is accepted alongside the short form dev@{0}."""
355 root, cids = _setup_repo_with_history(tmp_path, n_commits=2, branch="dev")
356
357 r = runner.invoke(
358 cli, ["rev-parse", "refs/heads/dev@{0}", "--json"], env=_env(root)
359 )
360
361 assert r.exit_code == 0, r.output
362 data = json.loads(r.output)
363 assert data["commit_id"] == cids[-1]
364
365 def test_head_log_and_branch_log_resolve_independently(
366 self, tmp_path: pathlib.Path
367 ) -> None:
368 """@{0} and main@{0} can differ when HEAD has moved since last branch op."""
369 root, cids = _setup_repo_with_history(tmp_path, n_commits=3, branch="main")
370
371 # @{0} uses HEAD log → newest commit on main
372 r_head = runner.invoke(cli, ["rev-parse", "@{0}", "--json"], env=_env(root))
373 # main@{0} uses main branch log → also newest (in this scenario they match)
374 r_branch = runner.invoke(
375 cli, ["rev-parse", "main@{0}", "--json"], env=_env(root)
376 )
377
378 assert r_head.exit_code == 0 and r_branch.exit_code == 0
379 assert json.loads(r_head.output)["commit_id"] == cids[-1]
380 assert json.loads(r_branch.output)["commit_id"] == cids[-1]
381
382
383 # ---------------------------------------------------------------------------
384 # RL_25 — Out-of-range index exits USER_ERROR with valid-range message
385 # ---------------------------------------------------------------------------
386
387 class TestAtNOutOfRange:
388 """RL_25: @{N} where N >= total entries → USER_ERROR with range in message."""
389
390 def test_out_of_range_rev_parse_exits_user_error(
391 self, tmp_path: pathlib.Path
392 ) -> None:
393 root, _ = _setup_repo_with_history(tmp_path, n_commits=2)
394
395 r = runner.invoke(cli, ["rev-parse", "@{99}", "--json"], env=_env(root))
396
397 assert r.exit_code == 1, r.output
398
399 def test_out_of_range_error_mentions_valid_range(
400 self, tmp_path: pathlib.Path
401 ) -> None:
402 """Error message must include the valid range so the user knows what's available."""
403 root, _ = _setup_repo_with_history(tmp_path, n_commits=2)
404 # 2 entries → valid indices are 0 and 1
405
406 r = runner.invoke(cli, ["rev-parse", "@{99}", "--json"], env=_env(root))
407
408 combined = r.output + (r.stderr or "")
409 # "0" and "1" (the valid range bounds) must appear
410 assert "0" in combined and "1" in combined
411
412 def test_out_of_range_reset_exits_user_error(
413 self, tmp_path: pathlib.Path
414 ) -> None:
415 root, _ = _setup_repo_with_history(tmp_path, n_commits=2)
416
417 r = runner.invoke(cli, ["reset", "@{99}", "--json"], env=_env(root))
418
419 assert r.exit_code != 0
420
421 def test_no_log_file_gives_user_error(self, tmp_path: pathlib.Path) -> None:
422 root = _make_base_repo(tmp_path)
423 # No reflog written
424
425 r = runner.invoke(cli, ["rev-parse", "@{0}", "--json"], env=_env(root))
426
427 assert r.exit_code == 1
428
429 def test_branch_at_n_out_of_range(self, tmp_path: pathlib.Path) -> None:
430 root, _ = _setup_repo_with_history(tmp_path, n_commits=1, branch="main")
431
432 r = runner.invoke(cli, ["rev-parse", "main@{5}", "--json"], env=_env(root))
433
434 assert r.exit_code == 1
435
436
437 # ---------------------------------------------------------------------------
438 # RL_26 — muse diff @{1} HEAD shows the diff between reflog state 1 and HEAD
439 # ---------------------------------------------------------------------------
440
441 class TestDiffAtN:
442 """RL_26: muse diff @{1} HEAD resolves both sides via reflog."""
443
444 def test_diff_at_one_head_exits_zero(self, tmp_path: pathlib.Path) -> None:
445 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
446
447 r = runner.invoke(
448 cli, ["diff", "@{1}", "HEAD", "--json"], env=_env(root)
449 )
450
451 assert r.exit_code == 0, r.output
452
453 def test_diff_at_one_head_has_required_json_fields(
454 self, tmp_path: pathlib.Path
455 ) -> None:
456 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
457
458 r = runner.invoke(
459 cli, ["diff", "@{1}", "HEAD", "--json"], env=_env(root)
460 )
461
462 data = json.loads(r.output)
463 for field in ("added", "modified", "deleted"):
464 assert field in data, f"missing field: {field!r}"
465
466 def test_diff_at_one_head_shows_changes(self, tmp_path: pathlib.Path) -> None:
467 """Each commit adds a new file so diff @{1} HEAD should show file2.txt added."""
468 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
469
470 r = runner.invoke(
471 cli, ["diff", "@{1}", "HEAD", "--json"], env=_env(root)
472 )
473
474 data = json.loads(r.output)
475 # commit-2 added file2.txt; commit-1 is @{1}
476 assert "file2.txt" in data["added"]
477
478 def test_diff_at_zero_head_is_empty(self, tmp_path: pathlib.Path) -> None:
479 """Diff between @{0} and HEAD should have no changes (same commit)."""
480 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
481
482 r = runner.invoke(
483 cli, ["diff", "@{0}", "HEAD", "--json"], env=_env(root)
484 )
485
486 data = json.loads(r.output)
487 assert data["total_changes"] == 0
488
489
490 # ---------------------------------------------------------------------------
491 # RL_27 — End-to-end recovery: commit A → commit B → reset @{1} → back at A
492 # ---------------------------------------------------------------------------
493
494 class TestEndToEndRecovery:
495 """RL_27: the full undo-safety-net workflow using @{N} as a commit ref."""
496
497 def test_reset_at_one_restores_previous_commit(
498 self, tmp_path: pathlib.Path
499 ) -> None:
500 """Commit A, commit B, then muse reset @{1} restores commit A."""
501 root, cids = _setup_repo_with_history(tmp_path, n_commits=2)
502 commit_a = cids[0] # oldest — reflog index 1
503 commit_b = cids[1] # newest — reflog index 0 (current HEAD)
504
505 # Sanity: HEAD is currently at commit B.
506 r = runner.invoke(cli, ["rev-parse", "HEAD", "--json"], env=_env(root))
507 assert json.loads(r.output)["commit_id"] == commit_b
508
509 # Recovery step: reset to the previous commit (index 1 = commit A).
510 r_reset = runner.invoke(cli, ["reset", "@{1}", "--json"], env=_env(root))
511 assert r_reset.exit_code == 0, r_reset.output
512
513 # Verify: HEAD is now at commit A.
514 r_read = runner.invoke(cli, ["read", "--json"], env=_env(root))
515 assert r_read.exit_code == 0, r_read.output
516 data = json.loads(r_read.output)
517 assert data["commit_id"] == commit_a
518
519 def test_message_at_restored_commit_is_correct(
520 self, tmp_path: pathlib.Path
521 ) -> None:
522 """After muse reset @{1}, muse read shows the message from commit A."""
523 root, cids = _setup_repo_with_history(tmp_path, n_commits=2)
524
525 runner.invoke(cli, ["reset", "@{1}"], env=_env(root))
526
527 r = runner.invoke(cli, ["read", "--json"], env=_env(root))
528 data = json.loads(r.output)
529 assert data["message"] == "commit-0"
530
531 def test_rev_parse_after_reset_shows_old_id(
532 self, tmp_path: pathlib.Path
533 ) -> None:
534 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
535 target = cids[0] # will be at @{2}
536
537 runner.invoke(cli, ["reset", "@{2}"], env=_env(root))
538
539 r = runner.invoke(cli, ["rev-parse", "HEAD", "--json"], env=_env(root))
540 assert json.loads(r.output)["commit_id"] == target
541
542 def test_three_commit_chain_recovery(self, tmp_path: pathlib.Path) -> None:
543 """Commit A → B → C, then reset @{2} restores A."""
544 root, cids = _setup_repo_with_history(tmp_path, n_commits=3)
545 commit_a, commit_b, commit_c = cids
546
547 runner.invoke(cli, ["reset", "@{2}"], env=_env(root))
548
549 r = runner.invoke(cli, ["rev-parse", "HEAD", "--json"], env=_env(root))
550 assert json.loads(r.output)["commit_id"] == commit_a
551
552
553 # ---------------------------------------------------------------------------
554 # Edge cases — resolve_reflog_ref
555 # ---------------------------------------------------------------------------
556
557 class TestResolveReflogRefUnit:
558 """Unit tests for the resolve_reflog_ref core function."""
559
560 def test_returns_none_for_non_at_spec(self, tmp_path: pathlib.Path) -> None:
561 from muse.core.reflog import resolve_reflog_ref
562
563 root = _make_base_repo(tmp_path)
564 assert resolve_reflog_ref("main", root) is None
565 assert resolve_reflog_ref("HEAD", root) is None
566 assert resolve_reflog_ref("sha256:abc", root) is None
567
568 def test_returns_new_id_for_valid_index(self, tmp_path: pathlib.Path) -> None:
569 from muse.core.reflog import resolve_reflog_ref
570
571 root, cids = _setup_repo_with_history(tmp_path, n_commits=2)
572
573 result = resolve_reflog_ref("@{0}", root)
574 assert result == cids[-1]
575
576 def test_raises_file_not_found_when_no_log(self, tmp_path: pathlib.Path) -> None:
577 from muse.core.reflog import resolve_reflog_ref
578
579 root = _make_base_repo(tmp_path)
580
581 with pytest.raises(FileNotFoundError):
582 resolve_reflog_ref("@{0}", root)
583
584 def test_raises_index_error_out_of_range(self, tmp_path: pathlib.Path) -> None:
585 from muse.core.reflog import resolve_reflog_ref
586
587 root, _ = _setup_repo_with_history(tmp_path, n_commits=2)
588
589 with pytest.raises(IndexError) as exc_info:
590 resolve_reflog_ref("@{99}", root)
591 idx, total = exc_info.value.args
592 assert idx == 99
593 assert total == 2
594
595 def test_branch_spec_reads_branch_log(self, tmp_path: pathlib.Path) -> None:
596 from muse.core.reflog import resolve_reflog_ref
597
598 root, cids = _setup_repo_with_history(tmp_path, n_commits=2, branch="feat")
599
600 result = resolve_reflog_ref("feat@{0}", root)
601 assert result == cids[-1]
602
603 def test_refs_heads_prefix_stripped(self, tmp_path: pathlib.Path) -> None:
604 from muse.core.reflog import resolve_reflog_ref
605
606 root, cids = _setup_repo_with_history(tmp_path, n_commits=2, branch="main")
607
608 result = resolve_reflog_ref("refs/heads/main@{0}", root)
609 assert result == cids[-1]
File History 1 commit
sha256:c6483b12ab17f2b6b1de0abb0b7f40fe0c53456c1e503f00136d903ed8d48e0e feat(checkout): accept @{N} reflog refs for detached-HEAD r… Sonnet 5 patch 2 days ago