gabriel / muse public
Open #48
filed by gabriel human · 25 days ago

muse branch -c and -m: reflog copy and rename parity with git

0 Anchors
Blast radius
Churn 30d
0 Proposals

muse branch -c / -m — reflog parity with git for copy and rename

Background

muse branch supports the full git flag surface — create, copy (-c/-C), rename (-m/-M), and delete (-d/-D). Branch create and delete are already handled correctly: create writes a branch: Created from <source> reflog entry; delete removes the reflog file. Copy and rename both have gaps relative to git's behaviour.

What git branch -c does that muse does not

git branch -c [<src>] <dest> does two things:

  1. Creates <dest> at the same commit as <src>.
  2. Copies the source branch's reflog (.git/logs/refs/heads/<src>) to the destination (.git/logs/refs/heads/<dest>), so the new branch appears to have inherited the full history of pointer movements from the source.

Muse's implementation (lines 636–680 of muse/cli/commands/branch.py) does step 1 but skips step 2 entirely, and also writes no reflog entry for the newly created branch. A branch created by copy has an empty reflog and no trace in the destination log that it came from anywhere.

What git branch -m does that muse does not

git branch -m [<old>] <new> does two things:

  1. Moves the branch ref (refs/heads/<old>refs/heads/<new>).
  2. Moves the reflog file (.git/logs/refs/heads/<old>.git/logs/refs/heads/<new>), preserving the full history under the new name. If the renamed branch was HEAD, it also writes a new entry to the HEAD log recording the rename.

Muse's implementation (lines 540–635 of muse/cli/commands/branch.py) does step 1 but leaves the old log file at the old path (orphaned), creates no log at the new path, and writes no reflog entry. After muse branch -m old new, muse reflog --branch new returns no entries even though the branch may have a rich history.

Clarification: -c does not check out

git branch -c and muse's -c both create without checking out — this is correct and intentional. Checkout-on-create is the job of muse checkout -b and muse switch -c, both of which already work correctly.

Goal

After this issue is complete:

  • muse branch -c src dst copies src's reflog to dst and writes a branch: Copied from src entry at the top of the destination log.
  • muse branch -m old new moves the reflog file from old to new and writes a branch: Renamed from old entry at the top of the destination log.
  • When muse branch -m renames the currently checked-out branch, a branch: Renamed from old to new entry is also written to the HEAD log.
  • muse branch -C (force copy) and muse branch -M (force rename) carry the same reflog semantics as their non-force counterparts.
  • All operations are safe with respect to partial failure: log files are written atomically and the branch ref is never left in an inconsistent state with the log.

Phases

Phase 1 — muse branch -c/-C: copy the reflog

When copying a branch, copy the source reflog and prepend a new entry marking the origin.

Semantics

# Before
.muse/logs/refs/heads/dev      ← 12 entries (full dev history)
.muse/logs/refs/heads/feat/x   ← does not exist

muse branch -c dev feat/x

# After
.muse/logs/refs/heads/dev      ← unchanged (12 entries)
.muse/logs/refs/heads/feat/x   ← 13 entries: 12 copied from dev +
                                   new entry "branch: Copied from dev"
                                   prepended at the top (newest-first means
                                   it will appear as @{0} in muse reflog)

The new entry at the top uses old_id = <null> (copy has no predecessor on the destination ref) and new_id = <tip of src>.

Deliverables

  • BC_01muse branch -c src dst copies .muse/logs/refs/heads/src to .muse/logs/refs/heads/dst; the copy is byte-for-byte identical except for the prepended entry
  • BC_02 — The prepended entry has operation branch: Copied from <src>, new_id = tip of src, old_id = null ID
  • BC_03muse reflog --branch dst --json after copy returns entries with the prepended entry at index 0 and the source history following
  • BC_04muse branch -C src dst (force copy, overwriting an existing branch) replaces the destination's reflog with the copied log; the previous destination log is not preserved
  • BC_05 — If the source branch has no reflog (new repo, never committed), copy writes just the single origin entry with no prior history
  • BC_06 — Reflog copy is atomic: written to a tmp file and renamed over the destination — if the process is interrupted after the ref write but before the log write, the branch is still valid (just missing the log)
  • BC_07 — Integration test: dev branch with 5 reflog entries → muse branch -c dev backup/devmuse reflog --branch backup/dev --json shows 6 entries (5 copied + 1 origin marker)

Test file: tests/test_cmd_branch_copy_reflog.py


Phase 2 — muse branch -m/-M: move the reflog

When renaming a branch, move its reflog file and write a rename entry.

Semantics

# Before
.muse/logs/refs/heads/feat/long-name    ← 8 entries

muse branch -m feat/long-name feat/short

# After
.muse/logs/refs/heads/feat/long-name    ← deleted
.muse/logs/refs/heads/feat/short        ← 9 entries: 8 moved + new rename entry

If the renamed branch is currently checked-out (HEAD points at it), also write to the HEAD log:

# HEAD log gains:
sha256:<tip>  sha256:<tip>  gabriel  <ts>  +0000\tbranch: Renamed from feat/long-name to feat/short

Note: old and new ID are identical (the commit didn't change, only the branch name).

Deliverables

  • BC_08muse branch -m old new moves .muse/logs/refs/heads/old to .muse/logs/refs/heads/new; old path is removed
  • BC_09 — The new log has a prepended entry with operation branch: Renamed from <old>, same new_id and old_id (both equal to the branch tip)
  • BC_10muse reflog --branch new --json after rename shows all prior entries (with updated path only — content unchanged) plus the rename entry at index 0
  • BC_11 — When renaming the currently checked-out branch, a branch: Renamed from <old> to <new> entry is appended to the HEAD log
  • BC_12muse branch -M old new (force rename) carries identical reflog semantics; the destination's prior log (if any) is discarded
  • BC_13 — Move is atomic: source log is not removed until the destination log write succeeds; if the destination write fails the source log is preserved
  • BC_14 — Parent directories for the destination path are created if needed (e.g. renaming dev to archive/old/dev)
  • BC_15 — Integration test: rename a 5-entry branch → muse reflog --branch new --json shows 6 entries → rename the currently checked-out branch → HEAD log gains one entry

Test file: tests/test_cmd_branch_rename_reflog.py


Phase 3 — muse branch operation entry standardisation

Audit all append_reflog calls in branch.py and checkout.py to ensure operation strings are consistent and complete.

Current state (from code inspection)

Operation append_reflog called? Operation string
branch <name> (create) branch: Created from <source>
branch -c (copy)
branch -m (rename)
branch -d (delete) n/a — log file removed
checkout -b ✅ (via checkout.py) branch: created from <source>

After Phase 3, every branch mutation (copy, rename) writes a reflog entry in addition to Phases 1 and 2. The operation string format across all branch operations is standardised:

Operation Standard string
Create branch: created from <source>
Copy branch: copied from <src>
Rename branch: renamed from <old>

Deliverables

  • BC_16 — Operation strings standardised to lowercase-first, present-tense form across branch.py and checkout.py
  • BC_17 — Audit test: for each branch mutation, muse reflog --json contains an entry whose operation starts with branch: and is in the correct standard form
  • BC_18 — No regression: existing tests in tests/test_cmd_branch.py and tests/test_cmd_reflog*.py remain green

Test file: additions to tests/test_cmd_branch.py and existing reflog test files


Acceptance Criteria

  • muse branch -c src dstmuse reflog --branch dst shows the source's full prior history plus a branch: copied from src entry.
  • muse branch -m old newmuse reflog --branch new shows the branch's full prior history plus a branch: renamed from old entry; the old log path is gone.
  • Renaming the checked-out branch writes to both the branch log and the HEAD log.
  • All 18 test IDs (BC_01BC_18) are green.
  • muse code test --json for changed files reports no regressions.

Out of Scope

  • Remote branch copy/rename (no reflog is maintained for remote-tracking refs).
  • muse branch --delete log archival — the current behaviour (delete the log on branch delete) is intentional.
  • Reflog expiry for orphaned log files — covered by the companion reflog lifecycle issue.
  • muse switch -c / muse checkout -b — these already write correct reflog entries.
Activity
gabriel opened this issue 25 days ago
No activity yet. Use the CLI to comment.