{"record":{"id":"a4510bde09d5d8b7","repo":"abhigyanpatwari/GitNexus","slug":"committed-promotion-base-is-not-an-immutable-objec","errorCode":null,"errorMessage":"committed promotion base is not an immutable object id","messagePattern":"committed promotion base is not an immutable object id","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/promotion_apply.py","lineNumber":538,"sourceCode":"    repo_root: Path = REPO_ROOT,\n    *,\n    ref: str = \"HEAD\",\n) -> dict[str, str]:\n    \"\"\"Bind targets to one immutable committed incumbent, never live edits.\"\"\"\n\n    _, payload = candidate_overlay_payload(overlay)\n    root, root_descriptor = _open_repository_root(repo_root)\n    os.close(root_descriptor)\n    rev = run_managed(\n        [\"git\", \"-C\", str(root), \"rev-parse\", f\"{ref}^{{commit}}\"],\n        timeout=60,\n        capture_stdout_bytes=256,\n    )\n    if not rev.ok or rev.stdout_capture_overflow or rev.stdout_capture is None:\n        raise ValueError(\"could not resolve the committed promotion base\")\n    commit = rev.stdout_capture.decode(\"ascii\", errors=\"strict\").strip()\n    if not commit or any(character not in \"0123456789abcdefABCDEF\" for character in commit):\n        raise ValueError(\"committed promotion base is not an immutable object id\")\n    bindings: dict[str, str] = {}\n    for relative, _content in payload:\n        for target in mirror_targets(relative):\n            key = target.as_posix()\n            if key in bindings:\n                raise ValueError(f\"duplicate overlay destination: {target}\")\n            result = run_managed(\n                [\"git\", \"-C\", str(root), \"show\", f\"{commit}:{key}\"],\n                timeout=60,\n                capture_stdout_bytes=MAX_CANDIDATE_OVERLAY_BYTES + 1,\n            )\n            if not result.ok or result.stdout_capture_overflow or result.stdout_capture is None:\n                raise ValueError(f\"committed overlay destination is unavailable: {target}\")\n            bindings[key] = hashlib.sha256(result.stdout_capture).hexdigest()\n    return bindings\n\n\ndef _write_recovery_artifact(","sourceCodeStart":520,"sourceCodeEnd":556,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/promotion_apply.py#L520-L556","documentation":"Thrown in `committed_destination_base_digests` when `git rev-parse` succeeded (rev.ok, captured stdout) but the decoded output is empty or contains characters outside `[0-9a-fA-F]`. The promoter treats this as 'the resolved object is not a 40/64-char hex object id' and refuses to bind promotion evidence to it.","triggerScenarios":"Calling `committed_destination_base_digests` with a ref that resolves to something other than a plain SHA — for example a symbolic ref printed as `ref: refs/heads/main`, a relative path, a `refs/...` textual name, or output that includes extra annotation lines. The hex-character scan rejects anything not a raw object id.","commonSituations":"Passing `ref='HEAD'` when HEAD is symbolic and rev-parse returned the symref text instead of the commit; using a ref expression whose output carries extra metadata; a corrupted or unusual git state; the `^{commit}` peeling did not yield a clean SHA (rare, e.g. a broken alternates setup).","solutions":["Resolve to an explicit SHA before calling: `ref = subprocess.check_output(['git','rev-parse', f'{ref}^{{commit}}']).decode().strip()` and pass that SHA.","Pin `ref` to a fully-qualified branch (`refs/heads/main`) or a tag, and ensure HEAD is not a dangling symref.","Reproduce the exact rev-parse output and confirm it is clean hex: `git -C <repo> rev-parse '<ref>^{commit}' | cat -A`.","If the repo uses worktrees, resolve from the main worktree to avoid symref leakage."],"exampleFix":"# before\nbases = committed_destination_base_digests(overlay, ref='HEAD')  # HEAD is symbolic\n# after\nsha = subprocess.check_output(\n    ['git', '-C', str(repo_root), 'rev-parse', 'HEAD^{commit}']\n).decode().strip()\nbases = committed_destination_base_digests(overlay, ref=sha)","handlingStrategy":"validation","validationCode":"import re, subprocess\n\n_HEX = re.compile(r\"^[0-9a-fA-F]+$\")\n\ndef ref_is_immutable_object_id(repo_root, ref: str) -> bool:\n    out = subprocess.run(\n        [\"git\", \"-C\", str(repo_root), \"rev-parse\", f\"{ref}^{{commit}}\"],\n        capture_output=True, text=True,\n    )\n    sha = out.stdout.strip()\n    return out.returncode == 0 and bool(sha) and bool(_HEX.match(sha))\n\n# resolve once, pass the SHA everywhere after\nsha = subprocess.check_output(\n    [\"git\", \"-C\", str(repo_root), \"rev-parse\", f\"{ref}^{{commit}}\"]\n).decode().strip()\ncommitted_destination_base_digests(overlay, ref=sha)","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Always peel with `^{commit}` and pass the resulting raw SHA, not the symbolic ref, into `committed_destination_base_digests`.","Avoid passing `HEAD` directly when HEAD may be a symref; resolve it first.","Add a CI assertion that the captured base is a 40- or 64-char hex string."],"tags":["git","object-id","validation","input-error"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}