gabriel / muse public
push-refspec-syntax.md markdown
152 lines 5.8 KB
Raw
sha256:b21e4b2bb9ba4ed001e7df7a087af5f1d0d4baa4c395fca07f50aedeff8de32c adding issues docs to bust staging mpack prebuild cache. Human 4 hours 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 1 commit
sha256:b21e4b2bb9ba4ed001e7df7a087af5f1d0d4baa4c395fca07f50aedeff8de32c adding issues docs to bust staging mpack prebuild cache. Human 4 hours ago