{"record":{"id":"0e8b3102d0be49a3","repo":"can1357/oh-my-pi","slug":"git-cat-file-returned-len-lines-lines-expected","errorCode":null,"errorMessage":"git cat-file returned {len(lines)} lines, expected {len(CACHE_KEY_PATHS)}: {proc.stdout!r}","messagePattern":"git cat-file returned (.+?) lines, expected (.+?): (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"python/robomp/src/natives_cache.py","lineNumber":159,"sourceCode":"\n    Raises ``subprocess.CalledProcessError`` if ``git`` itself fails (e.g.\n    not a repo) — callers SHOULD treat that as \"no cache\" and proceed.\n    \"\"\"\n    tgt = target if target is not None else target_triple()\n    stdin = \"\".join(f\"HEAD:{p}\\n\" for p in CACHE_KEY_PATHS)\n    proc = subprocess.run(\n        [\"git\", \"cat-file\", \"--batch-check\"],\n        input=stdin,\n        cwd=str(repo_dir),\n        text=True,\n        capture_output=True,\n        check=True,\n        env=_git_safe_directory_env(repo_dir),\n        timeout=120.0,\n    )\n    lines = proc.stdout.splitlines()\n    if len(lines) != len(CACHE_KEY_PATHS):\n        raise RuntimeError(\n            f\"git cat-file returned {len(lines)} lines, expected {len(CACHE_KEY_PATHS)}: {proc.stdout!r}\"\n        )\n    h = hashlib.sha256()\n    for path, line in zip(CACHE_KEY_PATHS, lines, strict=True):\n        stripped = line.strip()\n        if stripped.endswith(\"missing\"):\n            tree_hash = _NULL_TREE_HASH\n        else:\n            # \"<hash> <type> <size>\" — take the first token as the tree/blob hash.\n            tree_hash = stripped.split(None, 1)[0]\n        h.update(f\"{path}\\t{tree_hash}\\n\".encode())\n    h.update(f\"TARGET\\t{tgt}\\n\".encode())\n    return h.hexdigest()\n\n\ndef _repo_slug(repo: str) -> str:\n    \"\"\"Same convention as ``SandboxManager.pool_path``.\"\"\"\n    return repo.replace(\"/\", \"__\")","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/natives_cache.py#L141-L177","documentation":"`compute_key()` builds a deterministic sha256 cache key by piping `HEAD:<path>` requests for every entry of CACHE_KEY_PATHS ('crates', 'Cargo.lock', 'Cargo.toml', 'rust-toolchain.toml', 'packages/natives') into one `git cat-file --batch-check` invocation. It asserts the batch output contains exactly one line per requested path; if git returns fewer (or more) lines, the parse would silently misalign path-to-hash pairs, so a RuntimeError is raised with the raw stdout for diagnosis.","triggerScenarios":"Calling `compute_key(repo_dir)` (directly or via the NativesCache capture/populate flow) where `git cat-file --batch-check` does not emit exactly len(CACHE_KEY_PATHS)=5 lines: git aborts writing on one line early, a sparse/partial clone suppresses object lines, a git hook/config/filter alters batch-check output, or a fake git stub in tests emits truncated stdout. Called by tests like test_compute_key_deterministic_across_clones and test_capture_then_populate_shares_node_inode_but_copies_companions.","commonSituations":"Running against a shallow or partial (blobless) clone; a sparse-checkout worktree where `HEAD:crates` resolves unexpectedly; an old or patched git version in the runtime image whose batch-check formatting differs; a replace-ref or filter config changing output; a race where HEAD changes mid-invocation.","solutions":["Inspect the `proc.stdout` echoed in the message to see which lines came back and which path line is missing or extra","Reproduce manually in the repo: `printf 'HEAD:crates\\nHEAD:Cargo.lock\\nHEAD:Cargo.toml\\nHEAD:rust-toolchain.toml\\nHEAD:packages/natives\\n' | git cat-file --batch-check` and compare line count to 5","Check the repo is a normal full clone with the expected monorepo layout; re-clone without --filter/--sparse if needed","Verify the git version matches the one used in CI/Docker (the pi image pins its toolchain)","Catch the RuntimeError at the call site and treat it as 'no cache', proceeding with a normal build — the documented fallback posture for compute_key failures"],"exampleFix":"# before\nkey = compute_key(workspace_repo)\nentry = cache.lookup(repo, key)\n\n# after\ntry:\n    key = compute_key(workspace_repo)\nexcept (RuntimeError, subprocess.CalledProcessError, subprocess.TimeoutExpired):\n    key = None  # no cache; proceed with a normal build\nentry = cache.lookup(repo, key) if key else None","handlingStrategy":"try-catch","validationCode":"def repo_supports_cache_key(repo_dir):\n    # Missing paths are tolerated (null hash); the guard is that git works and the layout matches\n    out = subprocess.run([\"git\", \"rev-parse\", \"--is-inside-work-tree\"], cwd=repo_dir, capture_output=True)\n    return out.returncode == 0 and not (Path(repo_dir) / \".git\").is_file()  # avoid sparse/worktree edge cases","typeGuard":"def cat_file_output_well_formed(stdout: str, expected: int) -> bool:\n    return len(stdout.splitlines()) == expected","tryCatchPattern":"try:\n    key = compute_key(repo_dir)\nexcept (RuntimeError, subprocess.CalledProcessError, subprocess.TimeoutExpired):\n    key = None  # treat as cache miss and do a normal build","preventionTips":["Use a standard full clone (not sparse/partial) of the monorepo before invoking compute_key","Pin and verify the git version in the runtime image; smoke-test `git cat-file --batch-check` on the repo at startup","Always catch RuntimeError plus CalledProcessError/TimeoutExpired around compute_key and fall back to a cache miss","Keep the proc.stdout echoed in the message when reporting so misalignment is diagnosable"],"tags":["git","subprocess","cache-key","parsing"],"backgroundTag":"git-cat-file-unexpected-output","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}