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