update_ref.py python
231 lines 7.9 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago
1 """muse plumbing update-ref — move a branch HEAD to a specific commit.
2
3 Directly writes a branch reference file under ``.muse/refs/heads/``. This is
4 the lowest-level way to advance or rewind a branch without any merge logic.
5
6 Analogous to ``git update-ref``. Porcelain commands (``muse commit``,
7 ``muse merge``, ``muse reset``) call this internally after computing the new
8 commit ID.
9
10 Output::
11
12 {"branch": "main", "commit_id": "<sha256>", "previous": "<sha256> | null"}
13
14 Plumbing contract
15 -----------------
16
17 - Exit 0: ref updated.
18 - Exit 1: commit not found in the store, invalid commit ID format,
19 ``--delete`` on a non-existent ref, or CAS mismatch.
20 - Exit 3: file write failure.
21
22 Agent use — compare-and-swap (CAS)
23 -----------------------------------
24
25 In a multi-agent environment multiple agents may try to advance the same branch
26 concurrently. Use ``--old-value`` to make the update conditional: it succeeds
27 only if the current ref value matches the expected value. This turns update-ref
28 into an atomic compare-and-swap and prevents silent overwrites::
29
30 muse plumbing update-ref main <new_id> --old-value <expected_current_id>
31 """
32
33 from __future__ import annotations
34
35 import argparse
36 import json
37 import logging
38 import sys
39
40 from muse.core.errors import ExitCode
41 from muse.core.repo import require_repo
42 from muse.core.store import get_head_commit_id, read_commit, write_branch_ref
43 from muse.core.validation import validate_branch_name, validate_object_id
44
45 logger = logging.getLogger(__name__)
46
47 _FORMAT_CHOICES = ("json", "text")
48
49
50 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
51 """Register the update-ref subcommand."""
52 parser = subparsers.add_parser(
53 "update-ref",
54 help="Move a branch HEAD to a specific commit ID.",
55 description=__doc__,
56 formatter_class=argparse.RawDescriptionHelpFormatter,
57 )
58 parser.add_argument(
59 "branch",
60 help="Branch name to update.",
61 )
62 parser.add_argument(
63 "commit_id",
64 nargs="?",
65 default=None,
66 help="Commit ID to point the branch at. Omit with --delete to remove the branch.",
67 )
68 parser.add_argument(
69 "--delete", "-d",
70 action="store_true",
71 help="Delete the branch ref entirely.",
72 )
73 parser.add_argument(
74 "--no-verify",
75 dest="verify",
76 action="store_false",
77 help="Skip verifying the commit exists before updating.",
78 )
79 parser.add_argument(
80 "--old-value",
81 dest="old_value",
82 default=None,
83 metavar="COMMIT_ID",
84 help=(
85 "Compare-and-swap guard: update only if the current ref matches this commit ID. "
86 "Use 'null' to require that the ref does not currently exist. "
87 "Essential for safe concurrent updates in multi-agent environments."
88 ),
89 )
90 parser.add_argument(
91 "--format", "-f",
92 dest="fmt",
93 default="json",
94 metavar="FORMAT",
95 help="Output format: json (default) or text (silent on success).",
96 )
97 parser.add_argument(
98 "--json", action="store_const", const="json", dest="fmt",
99 help="Shorthand for --format json.",
100 )
101 parser.set_defaults(func=run, verify=True)
102
103
104 def run(args: argparse.Namespace) -> None:
105 """Move a branch HEAD to a specific commit ID.
106
107 Directly writes (or deletes) a branch ref file. When ``--verify`` is set
108 (the default), the commit must already exist in ``.muse/commits/``.
109 Pass ``--no-verify`` to write the ref even if the commit is not yet in
110 the local store (e.g. after ``muse plumbing unpack-objects``).
111
112 Pass ``--old-value <current_id>`` to perform a compare-and-swap: the
113 update is rejected if the ref currently points to a different commit.
114 This is the correct primitive for multi-agent branch updates.
115
116 Output (``--format json``, default)::
117
118 {"branch": "main", "commit_id": "<sha256>", "previous": "<sha256> | null"}
119
120 Output (``--format text``)::
121
122 (silent on success — exits 0)
123 """
124 fmt: str = args.fmt
125 branch: str = args.branch
126 commit_id: str | None = args.commit_id
127 delete: bool = args.delete
128 verify: bool = args.verify
129 old_value: str | None = args.old_value
130
131 if fmt not in _FORMAT_CHOICES:
132 print(
133 json.dumps({"error": f"Unknown format {fmt!r}. Valid: {', '.join(_FORMAT_CHOICES)}"}),
134 file=sys.stderr,
135 )
136 raise SystemExit(ExitCode.USER_ERROR)
137
138 root = require_repo()
139
140 try:
141 validate_branch_name(branch)
142 except ValueError as exc:
143 print(json.dumps({"error": f"Invalid branch name: {exc}"}), file=sys.stderr)
144 raise SystemExit(ExitCode.USER_ERROR)
145
146 ref_path = root / ".muse" / "refs" / "heads" / branch
147
148 # Validate --old-value before any write.
149 if old_value is not None and old_value != "null":
150 try:
151 validate_object_id(old_value)
152 except ValueError as exc:
153 print(json.dumps({"error": f"Invalid --old-value: {exc}"}), file=sys.stderr)
154 raise SystemExit(ExitCode.USER_ERROR)
155
156 if delete:
157 if not ref_path.exists():
158 print(json.dumps({"error": f"Branch ref does not exist: {branch}"}), file=sys.stderr)
159 raise SystemExit(ExitCode.USER_ERROR)
160 if old_value is not None:
161 current = ref_path.read_text(encoding="utf-8").strip()
162 if old_value != current:
163 print(
164 json.dumps({
165 "error": "CAS mismatch: ref does not match --old-value",
166 "current": current,
167 "expected": old_value,
168 }),
169 file=sys.stderr,
170 )
171 raise SystemExit(ExitCode.USER_ERROR)
172 ref_path.unlink()
173 if fmt == "json":
174 print(json.dumps({"branch": branch, "deleted": True}))
175 return
176
177 if commit_id is None:
178 print(json.dumps({"error": "commit_id is required unless --delete is used."}), file=sys.stderr)
179 raise SystemExit(ExitCode.USER_ERROR)
180
181 # Always validate the format — writing a malformed ID to a ref file would
182 # silently corrupt the repository regardless of the --verify flag.
183 try:
184 validate_object_id(commit_id)
185 except ValueError as exc:
186 print(json.dumps({"error": f"Invalid commit ID: {exc}"}), file=sys.stderr)
187 raise SystemExit(ExitCode.USER_ERROR)
188
189 if verify and read_commit(root, commit_id) is None:
190 print(json.dumps({"error": f"Commit not found in store: {commit_id}"}), file=sys.stderr)
191 raise SystemExit(ExitCode.USER_ERROR)
192
193 previous = get_head_commit_id(root, branch)
194
195 # CAS check — must happen after reading `previous` and before the write.
196 if old_value is not None:
197 if old_value == "null":
198 if previous is not None:
199 print(
200 json.dumps({
201 "error": "CAS mismatch: ref already exists (--old-value null requires no ref)",
202 "current": previous,
203 }),
204 file=sys.stderr,
205 )
206 raise SystemExit(ExitCode.USER_ERROR)
207 elif old_value != previous:
208 print(
209 json.dumps({
210 "error": "CAS mismatch: ref does not match --old-value",
211 "current": previous,
212 "expected": old_value,
213 }),
214 file=sys.stderr,
215 )
216 raise SystemExit(ExitCode.USER_ERROR)
217
218 try:
219 write_branch_ref(root, branch, commit_id)
220 except (OSError, ValueError) as exc:
221 print(json.dumps({"error": str(exc)}), file=sys.stderr)
222 raise SystemExit(ExitCode.INTERNAL_ERROR)
223
224 if fmt == "json":
225 print(
226 json.dumps({
227 "branch": branch,
228 "commit_id": commit_id,
229 "previous": previous,
230 })
231 )
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago