diff --git a/scripts/install.sh b/scripts/install.sh index 18e661bddfbc..4b03681d7d0e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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" diff --git a/tests/test_install_sh_diverged_update.py b/tests/test_install_sh_diverged_update.py new file mode 100644 index 000000000000..05f1e064d9f1 --- /dev/null +++ b/tests/test_install_sh_diverged_update.py @@ -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"(?Pgit 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"