gabriel / muse public
feat patch push feat/43-push-refspec #1 / 1
AI Agent gabriel · 1 day ago · Oct 22, 2026 · Diff

feat(push): refspec syntax local:remote for cross-branch pushes (muse#43)

Phases 1-3 of muse#43: - New parse_push_refspec(raw) -> (local_branch, remote_branch, force) in push.py. Parses git-style [+]<local>[:<remote>] syntax: a bare name keeps the existing single-name behavior (local == remote), "local:remote" decouples which local branch is read from which remote branch is advanced, and a leading "+" sets force for that one push. Empty local or remote side, or more than one ':', raises ValueError before any network call -- delete-by-refspec (":remote") is deliberately unsupported, since `muse push --delete <remote> <branch>` already covers that explicitly. - Wired into run(): local_branch drives every purely-local read (get_head_commit_id, read_reflog); remote_branch drives every remote- facing operation (remote_branch_heads lookups, get_remote_head/ set_remote_head, the unpack-mpack target, and the JSON `branch` field, which per the ticket always reflects the branch actually updated on the remote). --delete's branch argument is intentionally left unparsed -- deleting "dev:main" has no sensible meaning. - docs/agent-guide.md: new "refspec syntax for cross-branch pushes" section with the four canonical forms, plus a Gitism Glossary entry clarifying that push --json's `branch` field means the remote branch (relevant once local != remote), and a second entry noting refspec syntax now matches git exactly (a reassurance, not a gotcha). `muse agent-config sync` reports zero drift.

21 new tests in tests/test_push_refspec.py (RS_01-RS_10): 8 pure-function unit tests for parse_push_refspec, 5 integration tests driving the full mocked mpack-presign/PUT/unpack-mpack protocol through muse push <remote> <refspec> --json. Confirmed via genuine RED (reverted the run() wiring, kept parse_push_refspec itself since Phase 1 is independently unit-tested) -> all 5 integration tests failed with an uncaught validate_branch_name exception (proving the wiring, not just the parser, is load-bearing) -> GREEN.

Live-verified end-to-end against a real local musehub server (localhost:1337): created a disposable test repo, confirmed `muse push testremote dev:main` from the main branch advances remote main to local dev's tip without switching branches, confirmed remote never gained a "dev" branch, and confirmed `muse push testremote +dev:main` force-pushes past a diverged remote main with force=true correctly reaching the server. Test repo deleted after verification.

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

sha256:1c3d22f581aa652cf47f38f4dc32816f888bffc5fa53b579dc6f139f333a6090 sha
+49 ~6 symbols
3 changed · 1250 in snapshot files
sha256:b7ab32c3d48888d82109988bd6ad29a49e93178e80476a6468a56f578addab5f snapshot
+49
symbols added
~6
symbols modified
3
files changed
1250
files in snapshot
0
dead code introduced
Semantic Changes 55 symbols
~ tests/test_push_refspec.py .py 46 symbols added
+ TestParsePushRefspec class class TestParsePushRefspec L35–64
+ test_rs01_bare_name method method test_rs01_bare_name L36–37
+ test_rs02_local_colon_remote method method test_rs02_local_colon_remote L39–40
+ test_rs03_force_prefix_with_colon method method test_rs03_force_prefix_with_colon L42–43
+ test_rs04_force_prefix_bare_name method method test_rs04_force_prefix_bare_name L45–46
+ test_rs05_empty_local_side_raises method method test_rs05_empty_local_side_raises L52–54
+ test_rs05_empty_remote_side_raises method method test_rs05_empty_remote_side_raises L48–50
+ test_rs05_error_messages_are_clear method method test_rs05_error_messages_are_clear L60–64
+ test_rs05_too_many_colons_raises method method test_rs05_too_many_colons_raises L56–58
+ TestRefspecCrossBranch class class TestRefspecCrossBranch L164–190
+ test_rs07_dev_colon_main_advances_remote_main_not_dev method method test_rs07_dev_colon_main_advances_remote_main_not_dev L165–190
+ TestRefspecForcePrefix class class TestRefspecForcePrefix L193–213
+ test_rs08_force_prefix_equivalent_to_force_flag method method test_rs08_force_prefix_equivalent_to_force_flag L194–213
+ TestRefspecInvalid class class TestRefspecInvalid L242–263
+ test_rs10_trailing_colon_exits_nonzero_before_network_call method method test_rs10_trailing_colon_exits_nonzero_before_network_call L243–263
+ TestRefspecNewRemoteBranch class class TestRefspecNewRemoteBranch L216–239
+ test_rs09_pushing_to_nonexistent_remote_branch_creates_it method method test_rs09_pushing_to_nonexistent_remote_branch_creates_it L217–239
+ TestRefspecSameNameUnchanged class class TestRefspecSameNameUnchanged L140–161
+ test_rs06_dev_colon_dev_behaves_like_plain_dev method method test_rs06_dev_colon_dev_behaves_like_plain_dev L141–161
+ _bare_repo function function _bare_repo L72–83
+ _make_commit function function _make_commit L86–116
+ _mpack_push_transport function function _mpack_push_transport L119–137
+ cli variable variable cli L26–26
+ CliRunner import import CliRunner L24–24
+ CommitRecord import import CommitRecord L20–20
+ MagicMock import import MagicMock L11–11
+ Manifest import import Manifest L22–22
+ RemoteInfo import import RemoteInfo L16–16
+ SnapshotRecord import import SnapshotRecord L21–21
+ __version__ import import __version__ L15–15
+ annotations import import annotations L6–6
+ blob_id import import blob_id L22–22
+ compute_commit_id import import compute_commit_id L19–19
+ compute_snapshot_id import import compute_snapshot_id L19–19
+ datetime import import datetime L8–8
+ heads_dir import import heads_dir L18–18
+ json import import json L9–9
+ muse_dir import import muse_dir L18–18
+ parse_push_refspec import import parse_push_refspec L23–23
+ patch import import patch L11–11
+ pathlib import import pathlib L10–10
+ pytest import import pytest L13–13
+ write_commit import import write_commit L20–20
+ write_object import import write_object L17–17
+ write_snapshot import import write_snapshot L21–21
+ runner variable variable runner L27–27
~ docs/agent-guide.md .md 2 symbols added, 5 symbols modified
+ Refspec syntax — git push origin local:remote → muse push origin local:remote section Refspec syntax — git push origin local:remote → muse push origin local:remote L1461–1469
+ muse push — refspec syntax for cross-branch pushes section muse push — refspec syntax for cross-branch pushes L210–248
~ muse/cli/commands/push.py .py 1 symbol added, 1 symbol modified
+ parse_push_refspec function function parse_push_refspec L401–443
~ run
Files Changed
+1 ~2
1250 in snapshot
← Older Oldest on feat/43-push-refspec
All commits
Newer → Latest on feat/43-push-refspec

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:1c3d22f581aa652cf47f38f4dc32816f888bffc5fa53b579dc6f139f333a6090 --body "your comment"