gabriel / muse public
push-refspec-syntax.md markdown
152 lines 5.8 KB
Raw
sha256:c0eba5ad689cec79f4a3fcdc4f5da78556cb4b8cb7b330f944634356c10379ed chore: pivot to nightly channel — bump version to 0.2.0.dev… Sonnet 5 patch 12 days ago

push: refspec syntax local:remote for cross-branch pushes

Background

muse push <remote> <branch> always pushes the named local branch to the identically-named remote branch. There is no way to push a local branch to a differently-named remote branch without first switching to it — a friction point when doing release promotion (devmain) or cross-branch CI uploads from a detached state.

Git solved this with refspec syntax: git push origin local:remote. The colon separates the source (local) ref from the destination (remote) ref. Muse should support the same pattern.

Goal

# Push local main to remote main without leaving dev
muse push staging main:main

# Push local dev up to remote main (release promotion without switching)
muse push staging dev:main

# Existing single-name syntax unchanged
muse push staging dev          # → local dev → remote dev (unchanged)
muse push local dev            # → same

The + force prefix in refspec (git: +local:remote) is also supported as an alternative to --force:

muse push staging +dev:main    # → force-push local dev to remote main

How git does it

Git parses a positional <refspec> argument as [+]<src>:<dst>:

  • src — the local ref to send (branch name, or full refs/heads/…)
  • dst — the remote ref to update
  • + prefix — sets force flag for this refspec only

Git's push plumbing separates "what to send" (commit walk from src) from "what to update on the remote" (the dst ref). Muse's push protocol already has the same separation: the DAG walk uses the local branch tip, and the unpack-mpack call passes branch=<dst> to tell the server which ref to advance.

The only missing piece is the CLI: today the BRANCH positional is used for both roles. Adding a local:remote parse step decouples them.

Implementation

Refspec parsing

A small utility parse_push_refspec(raw: str) -> tuple[str, str, bool] that returns (local_branch, remote_branch, force). Called before any push logic runs. No changes to the push protocol or server.

Input local remote force
"dev" "dev" "dev" False
"dev:main" "dev" "main" False
"main:main" "main" "main" False
"+dev:main" "dev" "main" True
"+dev" "dev" "dev" True
"dev:" error
":main" error

Empty src or dst is an error — muse push --delete <remote> <branch> is the correct way to delete a remote branch.

Push command wiring

push.py calls parse_push_refspec on the BRANCH positional, derives local_branch and remote_branch, then:

  • Uses local_branch as the ref for the DAG walk (commit deduplication)
  • Passes remote_branch to the unpack-mpack call as the target branch name
  • If the refspec + prefix is set, OR --force is passed, force-push

JSON output

The branch field in push JSON output reflects the remote branch that was updated (what matters to the caller), not the local branch. This is consistent with current behavior where local == remote.

Phases

Phase 1 — Refspec parser + unit tests

New function parse_push_refspec (in push.py or a shared refspec.py). Pure function; no I/O.

Deliverables

  • [ ] RS_01parse_push_refspec("dev") returns ("dev", "dev", False)
  • [ ] RS_02parse_push_refspec("dev:main") returns ("dev", "main", False)
  • [ ] RS_03parse_push_refspec("+dev:main") returns ("dev", "main", True)
  • [ ] RS_04parse_push_refspec("+dev") returns ("dev", "dev", True)
  • [ ] RS_05parse_push_refspec("dev:") raises ValueError with clear message; same for ":main" and "dev:main:extra"

Phase 2 — Wire into push command + integration tests

Update muse/cli/commands/push.py to call parse_push_refspec and route the two branch names correctly through the push engine.

Deliverables

  • [ ] RS_06muse push local dev:dev --json succeeds; JSON branch == "dev"; remote dev advances (behaviorally identical to existing muse push local dev)
  • [ ] RS_07 — On a repo with commits on dev not on main: muse push local dev:main --json advances remote main to local dev's tip; remote dev is unchanged
  • [ ] RS_08muse push local +dev:main --json force-pushes; equivalent to muse push local dev:main --force
  • [ ] RS_09muse push local dev:nonexistent --json creates nonexistent on the remote (same as pushing a new branch by name — no special handling needed; server already supports this)
  • [ ] RS_10muse push local dev: --json exits 1 with "invalid refspec" error before any network call

Phase 3 — Docs

Update docs/agent-guide.md push section and the Gitism glossary to document the new syntax. Run muse agent-config sync afterwards.

Deliverables

  • [ ] RS_11 — refspec syntax documented in push section of agent-guide.md with the four canonical forms (same-name, cross-name, force prefix, existing --force flag still works)
  • [ ] RS_12muse agent-config sync reports no drift

Acceptance criteria

  1. muse push staging main:main from dev branch advances remote main to local main's tip without requiring a branch switch.
  2. muse push staging dev:main advances remote main to local dev's tip.
  3. muse push staging +dev:main force-pushes; rejects fast-forward check.
  4. Existing muse push staging dev behaviour is unchanged.
  5. All RS_01–RS_12 tests green.
  6. JSON branch field in push output always reflects the remote branch updated.

Out of scope

  • Delete via refspec (:remote) — use muse push --delete <remote> <branch>
  • Full git refspec syntax (refs/heads/…) — branch names only
  • Multi-refspec in one command (muse push local dev:main feat:feat) — single refspec only for now
File History 4 commits
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6 chore: bump version to 0.2.0.dev2 — nightly.2 Sonnet 4.6 patch 11 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 14 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9 docs: add issue docs for push have-negotiation bug (#55) an… Sonnet 4.6 18 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74 chore: trigger push to surface null-OID paths Sonnet 4.6 23 days ago