feat(@tools): add pull blocker self-healing logic

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-10 02:35:15 -07:00
parent 68c848dc56
commit 006bde3f6c

View file

@ -322,6 +322,48 @@ def enforce_hostname(name: str) -> None:
# Pull — propagate declared truth + this agent's own code
# ---------------------------------------------------------------------------
def _heal_pull_blockers(as_owner, repo_root: str, err: str) -> bool:
"""git refuses an ff merge over working files it would touch — even when
their content is byte-identical to the incoming commit (e.g. a tree that
was bridged by rsync before the same files landed upstream). Clearing only
exact matches is lossless: untracked twins are removed (the merge recreates
them verbatim), tracked edits matching the target are reset to HEAD (the
merge fast-forwards them straight back). Any file that differs stays put
and keeps blocking real local work is never destroyed."""
paths: list[str] = []
collect = False
for line in err.splitlines():
if "would be overwritten by" in line:
collect = True
elif collect and line.startswith(("\t", " ")):
paths.append(line.strip())
else:
collect = False
if not paths:
return False
healed = 0
for p in paths:
rc, want, _ = as_owner(["rev-parse", f"@{{u}}:{p}"], 15)
if rc != 0:
return False
rc, have, _ = as_owner(["hash-object", "--", p], 15)
if rc != 0 or have.strip() != want.strip():
logger.warning("pull blocked by %s which differs from upstream — leaving it", p)
return False
if as_owner(["ls-files", "--error-unmatch", "--", p], 15)[0] == 0:
if as_owner(["checkout", "--", p], 15)[0] != 0:
return False
else:
try:
os.unlink(os.path.join(repo_root, p))
except OSError as exc:
logger.warning("pull self-heal: cannot remove %s: %s", p, exc)
return False
healed += 1
logger.info("pull self-heal: cleared %d file(s) byte-identical to upstream", healed)
return True
def git_pull(repo_root: str, ctx: dict) -> bool:
"""ff-only pull as the REPO OWNER (root-owned .git objects would break the
autocommit service). Returns True iff HEAD moved (caller exits to restart)."""
@ -343,7 +385,10 @@ def git_pull(repo_root: str, ctx: dict) -> bool:
rc, before, _ = as_owner(["rev-parse", "HEAD"], 15)
if rc != 0:
return False
rc, _, err = as_owner(["pull", "--ff-only", "--quiet"])
for _ in range(3): # untracked + tracked blockers can surface in separate aborts
rc, _, err = as_owner(["pull", "--ff-only", "--quiet"])
if rc == 0 or not _heal_pull_blockers(as_owner, repo_root, err):
break
if rc != 0:
logger.warning("git pull failed (keeping current code/config): %s", err.strip()[:200])
return False