From 86fd570533e3c6853a4d523c19f645e728b659a6 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 26 Mar 2026 13:31:28 +0100 Subject: [PATCH] fix(consistency): correct behind-remote detection to not trigger on local-ahead _detect_behind_remote was comparing HEAD != @{u} which incorrectly triggered C-16 when the local repo had unpushed commits. Fixed to use git rev-list --count HEAD..@{u} which only counts commits the remote has that local lacks. Adds test_returns_false_when_local_ahead. Co-Authored-By: Claude Sonnet 4.6 --- state-hub/scripts/consistency_check.py | 20 +++++++++++--------- state-hub/tests/test_consistency_check.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/state-hub/scripts/consistency_check.py b/state-hub/scripts/consistency_check.py index 3532e24..167b8de 100644 --- a/state-hub/scripts/consistency_check.py +++ b/state-hub/scripts/consistency_check.py @@ -748,8 +748,10 @@ def _check_ghost_duplicates( # --------------------------------------------------------------------------- def _detect_behind_remote(repo_path: str) -> bool: - """Return True if the local repo is behind its remote tracking branch. + """Return True if the remote tracking branch has commits the local repo lacks. + "Ahead" (local has unpushed commits) is NOT considered behind. + "Diverged" is treated as behind (remote progress could be lost). Best-effort: returns False on any error (offline, no remote, etc.) so that check-only mode is never blocked by network issues. """ @@ -758,15 +760,15 @@ def _detect_behind_remote(repo_path: str) -> bool: ["git", "-C", repo_path, "fetch", "--quiet", "origin"], capture_output=True, timeout=15, ) - local = subprocess.run( - ["git", "-C", repo_path, "rev-parse", "HEAD"], + # Count commits in remote that are not in local. + # git rev-list HEAD..@{u} → commits remote has that local lacks. + result = subprocess.run( + ["git", "-C", repo_path, "rev-list", "--count", "HEAD..@{u}"], capture_output=True, text=True, timeout=5, - ).stdout.strip() - remote = subprocess.run( - ["git", "-C", repo_path, "rev-parse", "@{u}"], - capture_output=True, text=True, timeout=5, - ).stdout.strip() - return bool(local and remote and local != remote) + ) + if result.returncode != 0: + return False # no upstream configured or other error + return int(result.stdout.strip() or "0") > 0 except Exception: return False diff --git a/state-hub/tests/test_consistency_check.py b/state-hub/tests/test_consistency_check.py index 30c00f0..b368cda 100644 --- a/state-hub/tests/test_consistency_check.py +++ b/state-hub/tests/test_consistency_check.py @@ -450,6 +450,26 @@ class TestDetectBehindRemote: subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD"], capture_output=True) assert _detect_behind_remote(str(clone)) is False + def test_returns_false_when_local_ahead(self, tmp_path): + """Local has unpushed commits — NOT considered behind.""" + import subprocess + bare = tmp_path / "bare.git" + clone = tmp_path / "clone" + bare.mkdir() + subprocess.run(["git", "init", "--bare", str(bare)], capture_output=True) + subprocess.run(["git", "clone", str(bare), str(clone)], capture_output=True) + subprocess.run( + ["git", "-C", str(clone), "commit", "--allow-empty", "-m", "init"], + capture_output=True, + ) + subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD:main"], capture_output=True) + # Add a local-only commit (not pushed) + subprocess.run( + ["git", "-C", str(clone), "commit", "--allow-empty", "-m", "local only"], + capture_output=True, + ) + assert _detect_behind_remote(str(clone)) is False + def test_returns_true_when_behind(self, tmp_path): """Remote has a commit the clone doesn't → clone is behind.""" import subprocess