Skip to content

sync_deletions: is_shallow_repository cannot distinguish "not shallow" from "could not tell" #17118

Description

@mrveiss

Discovered during #16310 (PR #17117), scoped out rather than folded in.

What

`services/git_subprocess.py`'s `run_git()` collapses every failure mode
(non-zero exit, timeout, spawn error) into the same `("", 1)` result — a
caller cannot tell "git ran and reported an error" from "git could not be
run at all" (missing binary, permission error, path does not exist).

`is_shallow_repository()` reads that as:

```python
async def is_shallow_repository(repo_root: str) -> bool:
output, rc = await run_git(repo_root, "rev-parse", "--is-shallow-repository")
return rc == 0 and output.strip() == "true"
```

So a `repo_root` that is broken for any reason (not a git repo, .git
corrupted, git binary unavailable) reads as `False` — "not shallow" —
identical to a genuinely full-depth repository.

Where it matters

  • `compute_bootstrap_plan`'s existing shallow guard: the ambiguity is
    low-risk there, because a broken `repo_root` also fails the very next
    git calls in the same function (`_tracked_paths_at_commit`,
    `_ever_added_paths`), which correctly return an `error`-set
    `DeletionPlan`.
  • `ensure_full_history()` (new in deploy: the builtin updater never removes files deleted from source, so deleted modules pile up on every host #16310/fix(updater): unshallow code_source and re-bootstrap legacy markers (#16310) #17117): here the ambiguity is
    NOT self-correcting within the function — a broken `repo_root` makes
    `is_shallow_repository` return `False`, and `ensure_full_history`
    short-circuits to `return True, f"{repo_root} already has full
    history"` — a false success for a repo it never actually inspected.
    In the real call site (the ansible pre-flight task right after a
    successful `ansible.builtin.git` clone/update of the same directory)
    this is low-probability, and a broken `code_source` would still fail
    loudly at the next task that runs git against it — but the function's
    own contract is still wrong in isolation, and nothing currently catches
    it if it is ever called against an unverified path.

Suggested fix

Give `run_git` (or a new helper) a way to distinguish "ran, exit
non-zero" from "could not run at all" — e.g. raising on spawn/timeout
failure rather than returning the same shape as a clean non-zero exit, or
returning an explicit tri-state. Requires auditing every existing caller
of `run_git`/`is_shallow_repository` (`services/sync_deletions.py`,
`services/full_tree_drift.py`) since the contract change is
cross-cutting — out of scope for a single-purpose deletion-planner fix.

Activity

  1. added a commit that references this issue on Sep 20, 2026
  2. mrveiss commented on Sep 20, 2026

    @mrveiss
    OwnerAuthor

    Verified against merged main (8830293, post-#17147 consolidation). Not flagged by the consolidation as touched, but checked directly rather than assumed low-risk.

    • Tri-state fix — autobot-slm-backend/services/git_subprocess.py: ShallowCheck enum (SHALLOW/FULL/UNKNOWN) added; is_shallow_repository() returns it, mapping any non-zero run_git return code OR an unparseable rev-parse line to UNKNOWN rather than silently reading as "not shallow".
    • ensure_full_history no longer false-succeeds — explicit if status is ShallowCheck.UNKNOWN: return False, f"could not determine whether {repo_root} is a shallow clone" before the FULL-implies-success short-circuit; same UNKNOWN check repeated after the unshallow attempt.
    • Caller audit (both named in the issue):
      • services/sync_deletions.py:259-260: shallow_status = await is_shallow_repository(repo_root); if shallow_status is not ShallowCheck.FULL: — treats SHALLOW and UNKNOWN identically as "refuse to plan", with a reason string that names which case it was.
      • services/full_tree_drift.py: does not call is_shallow_repository at all (only run_git directly, for an unrelated ls-files call) — nothing to migrate there, confirmed by grep rather than assumed.

    Suggested-fix scope fully delivered and intact on merged main, verified by reading source directly. Closing stands.

    Correction: an earlier version of this comment additionally cited a local pytest re-run ("28 passed") as evidence. This project's standing rule treats a local run from a checkout as a design aid only, never evidence in a closing comment. Retracted — the file-read evidence above stands on its own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions