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 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 13:31:28 +01:00
parent f1b72aab82
commit 86fd570533
2 changed files with 31 additions and 9 deletions

View File

@@ -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

View File

@@ -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