"""muse plumbing symbolic-ref — read or write HEAD's symbolic reference. In Muse, HEAD is a symbolic reference that points to a branch (normal mode) or directly to a commit (detached HEAD state). This command reads which branch HEAD currently tracks, handles detached HEAD gracefully, or — with ``--set`` — updates HEAD to point to a different branch. Read mode output (JSON, default — normal branch HEAD):: { "ref": "HEAD", "symbolic_target": "refs/heads/main", "branch": "main", "commit_id": "", "detached": false } Read mode output (JSON — detached HEAD):: { "ref": "HEAD", "symbolic_target": null, "branch": null, "commit_id": "", "detached": true } When a branch has no commits yet, ``commit_id`` is ``null``. Write mode (``--set ``):: muse plumbing symbolic-ref HEAD --set main Output after a successful write:: { "ref": "HEAD", "symbolic_target": "refs/heads/main", "branch": "main", "commit_id": " | null", "detached": false } Text output (``--format text``, read mode):: refs/heads/main With ``--short``:: main Plumbing contract ----------------- - Exit 0: ref read or updated successfully. - Exit 1: ``--set`` target branch does not exist (unless ``--create-branch``); bad ``--format``; unsupported ref name. - Exit 3: I/O error reading or writing HEAD. Agent use --------- Read the current branch from any pipeline step:: muse plumbing symbolic-ref HEAD --short --format text # → main Check whether HEAD is detached:: muse plumbing symbolic-ref HEAD --json | python3 -c "import sys,json; print(json.load(sys.stdin)['detached'])" Point HEAD at a new empty branch (orphan-style):: muse plumbing symbolic-ref HEAD --set feat/new --create-branch --json Switch HEAD to an existing branch:: muse plumbing symbolic-ref HEAD --set main --json """ from __future__ import annotations import argparse import json import logging import pathlib import sys from typing import TypedDict from muse.core.errors import ExitCode from muse.core.repo import require_repo from muse.core.store import ( get_head_commit_id, read_head, write_head_branch, ) from muse.core.validation import sanitize_display, validate_branch_name logger = logging.getLogger(__name__) _FORMAT_CHOICES = ("json", "text") class _SymbolicRefResult(TypedDict): ref: str symbolic_target: str | None branch: str | None commit_id: str | None detached: bool def _read_symbolic_ref(root: pathlib.Path) -> _SymbolicRefResult: """Return the current HEAD symbolic-ref data. Handles both the normal (branch) and detached (commit) HEAD states without raising — detached HEAD is represented as a structured result with ``detached=True`` and ``branch=None``. Uses :func:`muse.core.store.read_head` directly so both states are covered, rather than :func:`read_current_branch` which raises on detached HEAD. """ state = read_head(root) if state["kind"] == "branch": branch = state["branch"] commit_id = get_head_commit_id(root, branch) return { "ref": "HEAD", "symbolic_target": f"refs/heads/{branch}", "branch": branch, "commit_id": commit_id, "detached": False, } # Detached HEAD — HEAD points directly to a commit. return { "ref": "HEAD", "symbolic_target": None, "branch": None, "commit_id": state["commit_id"], "detached": True, } def _branch_exists(root: pathlib.Path, branch: str) -> bool: """Return True if the branch ref file exists and is not a symlink. Symlinks are rejected for consistency with the object store and ref listing — a symlink at ``.muse/refs/heads/`` could point anywhere outside the repository. """ ref_path = root / ".muse" / "refs" / "heads" / branch return ref_path.is_file() and not ref_path.is_symlink() def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: """Register the symbolic-ref subcommand.""" parser = subparsers.add_parser( "symbolic-ref", help="Read or write HEAD's symbolic branch reference.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "ref", nargs="?", default="HEAD", help=( "The symbolic ref to query or update. " "Currently only HEAD is supported." ), ) parser.add_argument( "--set", "-s", default=None, dest="set_branch", metavar="BRANCH", help="Branch name to point HEAD at (write mode).", ) parser.add_argument( "--create-branch", action="store_true", dest="create_branch", help=( "With ``--set``, create the branch pointer even if the branch has " "no commits yet (orphan-style). Without this flag, ``--set`` " "requires the branch to already exist." ), ) parser.add_argument( "--format", "-f", dest="fmt", default="json", metavar="FORMAT", help="Output format: json or text. (default: json)", ) parser.add_argument( "--json", action="store_const", const="json", dest="fmt", help="Shorthand for --format json.", ) parser.add_argument( "--short", "-S", action="store_true", help=( "In text mode, emit only the branch name rather than the full " "``refs/heads/`` path. Ignored in detached HEAD state." ), ) parser.set_defaults(func=run) def run(args: argparse.Namespace) -> None: """Read or write HEAD's symbolic reference. With no ``--set`` flag, reads the current branch HEAD points to and the commit ID at that branch tip. Detached HEAD state is represented as a structured result rather than an error. With ``--set ``, updates HEAD to point to *branch*. By default the branch must already exist; use ``--create-branch`` to point HEAD at a branch with no commits yet (orphan mode used by ``muse init``). """ fmt: str = args.fmt ref: str = args.ref set_branch: str | None = args.set_branch create_branch: bool = args.create_branch short: bool = args.short 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) ref_upper = ref.upper() if ref_upper != "HEAD": print( json.dumps({"error": f"Unsupported ref {ref!r}. Only HEAD is supported."}), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) root = require_repo() # Write mode if set_branch is not None: try: validate_branch_name(set_branch) except ValueError as exc: print(json.dumps({"error": str(exc)}), file=sys.stderr) raise SystemExit(ExitCode.USER_ERROR) if not create_branch and not _branch_exists(root, set_branch): print( json.dumps({ "error": ( f"Branch {set_branch!r} does not exist. " "Use --create-branch to point HEAD at a new empty branch." ) }), file=sys.stderr, ) raise SystemExit(ExitCode.USER_ERROR) try: write_head_branch(root, set_branch) except OSError as exc: logger.debug("symbolic-ref write error: %s", exc) print(json.dumps({"error": str(exc)}), file=sys.stderr) raise SystemExit(ExitCode.INTERNAL_ERROR) commit_id = get_head_commit_id(root, set_branch) result: _SymbolicRefResult = { "ref": "HEAD", "symbolic_target": f"refs/heads/{set_branch}", "branch": set_branch, "commit_id": commit_id, "detached": False, } if fmt == "text": if short: print(sanitize_display(set_branch)) else: print(sanitize_display(f"refs/heads/{set_branch}")) return print(json.dumps(dict(result))) return # Read mode try: result = _read_symbolic_ref(root) except (OSError, ValueError) as exc: logger.debug("symbolic-ref read error: %s", exc) print(json.dumps({"error": str(exc)}), file=sys.stderr) raise SystemExit(ExitCode.INTERNAL_ERROR) if fmt == "text": if result["detached"]: # Detached HEAD has no branch name; emit the commit ID. commit = result["commit_id"] or "(no commit)" print(sanitize_display(f"(HEAD detached at {commit[:12]})")) elif short: print(sanitize_display(result["branch"] or "")) else: print(sanitize_display(result["symbolic_target"] or "")) return print(json.dumps(dict(result)))