"""muse plumbing update-ref — move a branch HEAD to a specific commit. Directly writes a branch reference file under ``.muse/refs/heads/``. This is the lowest-level way to advance or rewind a branch without any merge logic. Analogous to ``git update-ref``. Porcelain commands (``muse commit``, ``muse merge``, ``muse reset``) call this internally after computing the new commit ID. Output:: {"branch": "main", "commit_id": "", "previous": " | null"} Plumbing contract ----------------- - Exit 0: ref updated. - Exit 1: commit not found in the store, invalid commit ID format, ``--delete`` on a non-existent ref, or CAS mismatch. - Exit 3: file write failure. Agent use — compare-and-swap (CAS) ----------------------------------- In a multi-agent environment multiple agents may try to advance the same branch concurrently. Use ``--old-value`` to make the update conditional: it succeeds only if the current ref value matches the expected value. This turns update-ref into an atomic compare-and-swap and prevents silent overwrites:: muse plumbing update-ref main --old-value """ from __future__ import annotations import argparse import json import logging import sys from muse.core.errors import ExitCode from muse.core.repo import require_repo from muse.core.store import get_head_commit_id, read_commit, write_branch_ref from muse.core.validation import validate_branch_name, validate_object_id logger = logging.getLogger(__name__) _FORMAT_CHOICES = ("json", "text") def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: """Register the update-ref subcommand.""" parser = subparsers.add_parser( "update-ref", help="Move a branch HEAD to a specific commit ID.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "branch", help="Branch name to update.", ) parser.add_argument( "commit_id", nargs="?", default=None, help="Commit ID to point the branch at. Omit with --delete to remove the branch.", ) parser.add_argument( "--delete", "-d", action="store_true", help="Delete the branch ref entirely.", ) parser.add_argument( "--no-verify", dest="verify", action="store_false", help="Skip verifying the commit exists before updating.", ) parser.add_argument( "--old-value", dest="old_value", default=None, metavar="COMMIT_ID", help=( "Compare-and-swap guard: update only if the current ref matches this commit ID. " "Use 'null' to require that the ref does not currently exist. " "Essential for safe concurrent updates in multi-agent environments." ), ) parser.add_argument( "--format", "-f", dest="fmt", default="json", metavar="FORMAT", help="Output format: json (default) or text (silent on success).", ) parser.add_argument( "--json", action="store_const", const="json", dest="fmt", help="Shorthand for --format json.", ) parser.set_defaults(func=run, verify=True) def run(args: argparse.Namespace) -> None: """Move a branch HEAD to a specific commit ID. Directly writes (or deletes) a branch ref file. When ``--verify`` is set (the default), the commit must already exist in ``.muse/commits/``. Pass ``--no-verify`` to write the ref even if the commit is not yet in the local store (e.g. after ``muse plumbing unpack-objects``). Pass ``--old-value `` to perform a compare-and-swap: the update is rejected if the ref currently points to a different commit. This is the correct primitive for multi-agent branch updates. Output (``--format json``, default):: {"branch": "main", "commit_id": "", "previous": " | null"} Output (``--format text``):: (silent on success — exits 0) """ fmt: str = args.fmt branch: str = args.branch commit_id: str | None = args.commit_id delete: bool = args.delete verify: bool = args.verify old_value: str | None = args.old_value if fmt not in _FORMAT_CHOICES: print( json.dumps({"error": f"Unknown format {fmt!r}. Valid: {', '.join(_FORMAT_CHOICES)}"}), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) root = require_repo() try: validate_branch_name(branch) except ValueError as exc: print(json.dumps({"error": f"Invalid branch name: {exc}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) ref_path = root / ".muse" / "refs" / "heads" / branch # Validate --old-value before any write. if old_value is not None and old_value != "null": try: validate_object_id(old_value) except ValueError as exc: print(json.dumps({"error": f"Invalid --old-value: {exc}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) if delete: if not ref_path.exists(): print(json.dumps({"error": f"Branch ref does not exist: {branch}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) if old_value is not None: current = ref_path.read_text(encoding="utf-8").strip() if old_value != current: print( json.dumps({ "error": "CAS mismatch: ref does not match --old-value", "current": current, "expected": old_value, }), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) ref_path.unlink() if fmt == "json": print(json.dumps({"branch": branch, "deleted": True})) return if commit_id is None: print(json.dumps({"error": "commit_id is required unless --delete is used."}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) # Always validate the format — writing a malformed ID to a ref file would # silently corrupt the repository regardless of the --verify flag. try: validate_object_id(commit_id) except ValueError as exc: print(json.dumps({"error": f"Invalid commit ID: {exc}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) if verify and read_commit(root, commit_id) is None: print(json.dumps({"error": f"Commit not found in store: {commit_id}"}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) previous = get_head_commit_id(root, branch) # CAS check — must happen after reading `previous` and before the write. if old_value is not None: if old_value == "null": if previous is not None: print( json.dumps({ "error": "CAS mismatch: ref already exists (--old-value null requires no ref)", "current": previous, }), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) elif old_value != previous: print( json.dumps({ "error": "CAS mismatch: ref does not match --old-value", "current": previous, "expected": old_value, }), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) try: write_branch_ref(root, branch, commit_id) except (OSError, ValueError) as exc: print(json.dumps({"error": str(exc)}), file=sys.stderr) raise SystemExit(ExitCode.INTERNAL_ERROR) if fmt == "json": print( json.dumps({ "branch": branch, "commit_id": commit_id, "previous": previous, }) )