Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,21 @@ clone_repo() {
git remote set-branches origin "$BRANCH" 2>/dev/null || true
git fetch origin "$BRANCH"
git checkout "$BRANCH"
git pull --ff-only origin "$BRANCH"
# Managed installs should follow origin/$BRANCH exactly. If both
# the local clone and the fetched remote advanced, fast-forwarding
# is impossible; mirror `hermes update` and reset to the fetched
# remote so the installer can recover instead of aborting.
local rev_counts local_ahead local_behind
rev_counts="$(git rev-list --left-right --count HEAD...origin/$BRANCH 2>/dev/null || true)"
set -- $rev_counts
local_ahead="${1:-0}"
local_behind="${2:-0}"
if [ "$local_ahead" -gt 0 ] && [ "$local_behind" -gt 0 ]; then
log_warn "History diverged from origin/$BRANCH; resetting managed install to the remote branch..."
git reset --hard "origin/$BRANCH"
else
git pull --ff-only origin "$BRANCH"
fi

if [ -n "$autostash_ref" ]; then
local restore_now="yes"
Expand Down
52 changes: 52 additions & 0 deletions tests/test_install_sh_diverged_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Regression tests for install.sh updates from a diverged managed clone.

Issue #53257 showed ``scripts/install.sh`` aborting during updates when the
managed checkout had both local-only and remote-only commits. ``git pull
--ff-only`` cannot resolve that state, so the installer must detect the
divergence after ``git fetch`` and hard-reset the managed clone to
``origin/$BRANCH`` instead of exiting with Git's fast-forward error.
"""

from __future__ import annotations

import re
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parent.parent
INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"


def _extract_clone_repo_update_block() -> str:
text = INSTALL_SH.read_text()
match = re.search(
r"(?P<block>git remote set-branches origin \"\$BRANCH\".*?git pull --ff-only origin \"\$BRANCH\".*?fi)",
text,
re.DOTALL,
)
assert match is not None, (
"Could not locate the managed-install update block in scripts/install.sh"
)
return match["block"]


def test_install_script_detects_diverged_history_before_pull() -> None:
block = _extract_clone_repo_update_block()

assert 'git rev-list --left-right --count HEAD...origin/$BRANCH' in block
assert 'if [ "$local_ahead" -gt 0 ] && [ "$local_behind" -gt 0 ]; then' in block
assert 'git reset --hard "origin/$BRANCH"' in block


def test_install_script_resets_diverged_clone_instead_of_aborting() -> None:
block = _extract_clone_repo_update_block()

rev_idx = block.find('git rev-list --left-right --count HEAD...origin/$BRANCH')
reset_idx = block.find('git reset --hard "origin/$BRANCH"')
pull_idx = block.find('git pull --ff-only origin "$BRANCH"')

assert rev_idx != -1, "expected divergence probe in clone_repo()"
assert reset_idx != -1, "expected reset fallback in clone_repo()"
assert pull_idx != -1, "expected ff-only pull path in clone_repo()"
assert rev_idx < reset_idx, "divergence probe must run before the reset fallback"
assert rev_idx < pull_idx, "divergence probe must run before the ff-only pull"
Loading