gabriel / muse public
feat patch branch feat/48-branch-copy-rename-reflog #1 / 1
AI Agent gabriel · 52 minutes ago · Oct 22, 2026 · Diff

feat(branch): reflog copy/rename parity with git, standardize operation strings (muse#48)

Phases 1-3 of muse#48: - New muse/core/reflog.py::copy_branch_reflog — mirrors `git branch -c`: copies the source branch's reflog to the destination and appends one new "branch: copied from <src>" entry (old_id=null, new_id=src tip). Reflog files are oldest-first on disk (read_reflog reverses for @{0} display), so the new entry is appended after the copied bytes, not prepended before them, despite the ticket's "prepended at the top" phrasing (which describes display order, not file bytes). - New rename_branch_reflog — mirrors `git branch -m`: moves the reflog file and appends a "branch: renamed from <old>" entry (old_id=new_id=tip, since the commit doesn't change). Destination written and os.replace'd before the source is unlinked, so a failed write preserves the original. - New append_head_only_reflog: when renaming the checked-out branch, the HEAD log needs its own "renamed from X to Y" entry without double-writing the branch log (append_reflog always writes both logs). - branch.py's create/copy/rename paths wired to the above; operation strings standardized to lowercase-first present-tense ("branch: created from", "branch: copied from", "branch: renamed from") matching checkout.py's existing convention, replacing the old inconsistent "branch: Created from" capitalization.

Real bug found and fixed while building this: append_reflog's on-disk line format collapses to 4 whitespace-split tokens (instead of the 5 _parse_line requires) whenever author="" — which branch.py's create path had used since inception — making every "branch: created from X" entry silently unparseable and invisible to read_reflog/muse reflog. Fixed by switching branch.py's reflog writes to author="user", matching checkout.py's convention; the blast radius was isolated to branch.py (the only writer that ever used an empty author).

18 new/updated tests across tests/test_cmd_branch_copy_reflog.py (BC_01-07), tests/test_cmd_branch_rename_reflog.py (BC_08-15), and additions to tests/test_cmd_branch.py (BC_16-18) — all confirmed via genuine RED (reverted the branch.py wiring, 13/17 new tests failed as expected) → GREEN. Live-verified via muse-dev against a disposable /tmp repo: copy/rename both preserve full history, HEAD log correctly records a checked-out rename, old reflog path removed after rename.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:f7bf283e6cd64892d61560b1d4508b52612b0d8cd2c7d02a17fcdbbd3ae75b66 sha
+51 ~4 symbols
5 changed · 1249 in snapshot files
sha256:746c8638da28565755fd7730cccf75668546d83753ea09c55c5052e5083f142a snapshot
+51
symbols added
~4
symbols modified
5
files changed
1249
files in snapshot
0
dead code introduced
Semantic Changes 55 symbols
~ tests/test_cmd_branch_copy_reflog.py .py 19 symbols added
+ TestCopyReflogBasic class class TestCopyReflogBasic L23–171
+ test_bc01_log_copied_byte_for_byte_plus_new_entry method method test_bc01_log_copied_byte_for_byte_plus_new_entry L24–47
+ test_bc02_prepended_entry_fields method method test_bc02_prepended_entry_fields L49–60
+ test_bc03_reflog_cli_shows_prepended_then_source_history method method test_bc03_reflog_cli_shows_prepended_then_source_history L62–78
+ test_bc04_force_copy_replaces_destination_log method method test_bc04_force_copy_replaces_destination_log L80–99
+ test_bc05_source_with_no_reflog_writes_just_origin_entry method method test_bc05_source_with_no_reflog_writes_just_origin_entry L101–126
+ test_bc05b_source_with_reflog_but_dest_from_scratch method method test_bc05b_source_with_reflog_but_dest_from_scratch L128–137
+ test_bc06_ref_written_before_reflog_copy_is_safe_if_interrupted method method test_bc06_ref_written_before_reflog_copy_is_safe_if_interrupted L139–152
+ test_bc07_integration_five_entries_copy_yields_six method method test_bc07_integration_five_entries_copy_yields_six L154–171
+ NULL_COMMIT_ID import import NULL_COMMIT_ID L19–19
+ _branch import import _branch L20–20
+ _commit import import _commit L20–20
+ _invoke import import _invoke L20–20
+ annotations import import annotations L11–11
+ pathlib import import pathlib L13–13
+ pytest import import pytest L15–15
+ read_reflog import import read_reflog L18–18
+ reflog_branch_path import import reflog_branch_path L17–17
+ repo import import repo L20–20
+ TestRenameReflogBasic class class TestRenameReflogBasic L24–188
+ test_bc08_log_moved_old_path_removed method method test_bc08_log_moved_old_path_ L25–35
+ test_bc09_appended_entry_old_id_equals_new_id_equals_tip method method test_bc09_appended_entry_old_id_equals_new_id_equals_tip L37–51
+ test_bc10_prior_entries_preserved_plus_rename_entry method method test_bc10_prior_entries_preserved_plus_rename_entry L53–70
+ test_bc11_renaming_checked_out_branch_writes_head_log_entry method method test_bc11_renaming_checked_out_branch_writes_head_log_entry L72–91
+ test_bc11b_renaming_non_checked_out_branch_does_not_touch_head_log method method test_bc11b_renaming_non_checked_out_branch_does_not_touch_head_log L93–104
+ test_bc12_force_rename_same_reflog_semantics method method test_bc12_force_rename_same_reflog_semantics L106–124
+ test_bc13_source_preserved_if_destination_write_fails method method test_bc13_source_preserved_if_destination_write_fails L126–151
+ test_bc14_parent_directories_created_for_destination method method test_bc14_parent_directories_created_for_destination L153–161
+ test_bc15_integration_five_entries_then_checked_out_rename method method test_bc15_integration_five_entries_then_checked_out_rename L163–188
+ _branch import import _branch L21–21
+ _commit import import _commit L21–21
+ _invoke import import _invoke L21–21
+ annotations import import annotations L12–12
+ pathlib import import pathlib L14–14
+ pytest import import pytest L16–16
+ read_current_branch import import read_current_branch L20–20
+ read_reflog import import read_reflog L19–19
+ reflog_branch_path import import reflog_branch_path L18–18
+ repo import import repo L21–21
~ muse/cli/commands/branch.py .py 3 symbols added, 2 symbols modified
+ append_head_only_reflog import import append_head_only_reflog L62–62
+ copy_branch_reflog import import copy_branch_reflog L62–62
+ rename_branch_reflog import import rename_branch_reflog L62–62
~ run
~ muse/core/reflog.py .py 3 symbols added
+ append_head_only_reflog function function append_head_only_reflog L163–189
+ copy_branch_reflog function function copy_branch_reflog L191–245
+ rename_branch_reflog function function rename_branch_reflog L247–304
~ tests/test_cmd_branch.py .py 6 symbols added, 2 symbols modified
+ TestOperationStringStandardisation class class TestOperationStringStandardisation L1080–1136
+ test_bc16_copy_operation_is_lowercase method method test_bc16_copy_operation_is_lowercase L1093–1099
+ test_bc16_create_operation_is_lowercase method method test_bc16_create_operation_is_lowercase L1086–1091
+ test_bc16_rename_operation_is_lowercase method method test_bc16_rename_operation_is_lowercase L1101–1107
+ test_bc17_every_branch_mutation_has_standard_form_entry method method test_bc17_every_branch_mutation_has_standard_form_entry L1109–1130
+ test_bc18_no_regression_existing_branch_and_reflog_tests_pass method method test_bc18_no_regression_existing_branch_and_reflog_tests_pass L1132–1136
Files Changed
+2 ~3
1249 in snapshot
← Older Oldest on feat/48-branch-copy-rename-reflog
All commits
Newer → Latest on feat/48-branch-copy-rename-reflog

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:f7bf283e6cd64892d61560b1d4508b52612b0d8cd2c7d02a17fcdbbd3ae75b66 --body "your comment"