mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(install): continue after autostash restore conflicts
This commit is contained in:
parent
21f971bbab
commit
de90f5831b
3 changed files with 185 additions and 9 deletions
|
|
@ -1553,15 +1553,35 @@ function Install-Repository {
|
|||
|
||||
if ($restoreNow) {
|
||||
Write-Info "Restoring local changes..."
|
||||
git -c windows.appendAtomically=false stash apply $autostashRef
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$restoreOutput = @(git -c windows.appendAtomically=false stash apply $autostashRef 2>&1)
|
||||
$restoreExit = $LASTEXITCODE
|
||||
$conflictedFiles = @(
|
||||
git -c windows.appendAtomically=false diff --name-only --diff-filter=U 2>$null
|
||||
) | Where-Object { $_ -and $_.ToString().Trim() }
|
||||
if (($restoreExit -eq 0) -and ($conflictedFiles.Count -eq 0)) {
|
||||
git -c windows.appendAtomically=false stash drop $autostashRef 2>$null
|
||||
Write-Warn "Local changes were restored on top of the updated codebase."
|
||||
Write-Warn "Review git diff / git status if Hermes behaves unexpectedly."
|
||||
} else {
|
||||
Write-Err "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash."
|
||||
Write-Info "Resolve manually with: git stash apply $autostashRef"
|
||||
throw "git stash apply failed after update"
|
||||
Write-Err "Update pulled new code, but restoring local changes hit conflicts."
|
||||
foreach ($line in $restoreOutput) {
|
||||
if ($line -and $line.ToString().Trim()) {
|
||||
Write-Host $line
|
||||
}
|
||||
}
|
||||
if ($conflictedFiles.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Conflicted files:"
|
||||
foreach ($file in $conflictedFiles) {
|
||||
Write-Host " • $file"
|
||||
}
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Info "Your stashed changes are preserved — nothing is lost."
|
||||
Write-Info " Stash ref: $autostashRef"
|
||||
git -c windows.appendAtomically=false reset --hard HEAD 2>$null | Out-Null
|
||||
Write-Info "Working tree reset to clean state."
|
||||
Write-Info "Restore your changes later with: git stash apply $autostashRef"
|
||||
}
|
||||
} else {
|
||||
Write-Info "Skipped restoring local changes."
|
||||
|
|
|
|||
|
|
@ -1247,14 +1247,38 @@ clone_repo() {
|
|||
|
||||
if [ "$restore_now" = "yes" ]; then
|
||||
log_info "Restoring local changes..."
|
||||
if git stash apply "$autostash_ref"; then
|
||||
local restore_output=""
|
||||
local restore_ok="yes"
|
||||
if restore_output="$(git stash apply "$autostash_ref" 2>&1)"; then
|
||||
restore_ok="yes"
|
||||
else
|
||||
restore_ok="no"
|
||||
fi
|
||||
local conflicted_files=""
|
||||
conflicted_files="$(git diff --name-only --diff-filter=U || true)"
|
||||
if [ "$restore_ok" = "yes" ] && [ -z "$conflicted_files" ]; then
|
||||
git stash drop "$autostash_ref" >/dev/null
|
||||
log_warn "Local changes were restored on top of the updated codebase."
|
||||
log_warn "Review git diff / git status if Hermes behaves unexpectedly."
|
||||
else
|
||||
log_error "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash."
|
||||
log_info "Resolve manually with: git stash apply $autostash_ref"
|
||||
exit 1
|
||||
log_error "Update pulled new code, but restoring local changes hit conflicts."
|
||||
if [ -n "$restore_output" ]; then
|
||||
printf '%s\n' "$restore_output"
|
||||
fi
|
||||
if [ -n "$conflicted_files" ]; then
|
||||
printf '\nConflicted files:\n'
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] && printf ' • %s\n' "$file"
|
||||
done <<EOF
|
||||
$conflicted_files
|
||||
EOF
|
||||
fi
|
||||
printf '\n'
|
||||
log_info "Your stashed changes are preserved — nothing is lost."
|
||||
log_info " Stash ref: $autostash_ref"
|
||||
git reset --hard HEAD >/dev/null 2>&1 || true
|
||||
log_info "Working tree reset to clean state."
|
||||
log_info "Restore your changes later with: git stash apply $autostash_ref"
|
||||
fi
|
||||
else
|
||||
log_info "Skipped restoring local changes."
|
||||
|
|
|
|||
132
tests/test_install_autostash_conflict_recovery.py
Normal file
132
tests/test_install_autostash_conflict_recovery.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"""Regression: installer autostash restore conflicts must not abort the run.
|
||||
|
||||
An interrupted/repeated managed install can leave local tracked edits in the
|
||||
checkout. If upstream then changes the same lines, ``git stash apply`` conflicts
|
||||
during the repository-update stage. Both installers must leave the stash intact,
|
||||
reset the worktree clean, and complete the real repository stage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
|
||||
INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1"
|
||||
POWERSHELL = next(
|
||||
(candidate for candidate in ("pwsh", "powershell") if shutil.which(candidate)),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _git(cwd: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
["git", "-c", "user.email=t@t", "-c", "user.name=t", *args],
|
||||
cwd=cwd,
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def _make_conflicted_managed_checkout(tmp_path: Path) -> Path:
|
||||
"""Create a managed checkout whose autostash conflicts with its origin."""
|
||||
seed = tmp_path / "seed"
|
||||
seed.mkdir()
|
||||
_git(seed, "init")
|
||||
(seed / "tracked.txt").write_text("base\n", encoding="utf-8")
|
||||
_git(seed, "add", "tracked.txt")
|
||||
_git(seed, "commit", "-m", "base")
|
||||
_git(seed, "branch", "-M", "main")
|
||||
|
||||
remote = tmp_path / "origin.git"
|
||||
_git(tmp_path, "init", "--bare", str(remote))
|
||||
_git(seed, "remote", "add", "origin", str(remote))
|
||||
_git(seed, "push", "-u", "origin", "main")
|
||||
|
||||
managed = tmp_path / "hermes-agent"
|
||||
_git(tmp_path, "clone", "--branch", "main", str(remote), str(managed))
|
||||
|
||||
(managed / "tracked.txt").write_text("local edit\n", encoding="utf-8")
|
||||
|
||||
upstream = tmp_path / "upstream"
|
||||
_git(tmp_path, "clone", "--branch", "main", str(remote), str(upstream))
|
||||
(upstream / "tracked.txt").write_text("upstream edit\n", encoding="utf-8")
|
||||
_git(upstream, "commit", "-am", "upstream")
|
||||
_git(upstream, "push", "origin", "main")
|
||||
|
||||
return managed
|
||||
|
||||
|
||||
def _assert_conflict_was_recovered(repo: Path, output: str) -> None:
|
||||
assert "restoring local changes hit conflicts" in output
|
||||
assert "Conflicted files:" in output
|
||||
assert "tracked.txt" in output
|
||||
assert "Working tree reset to clean state." in output
|
||||
assert "Restore your changes later with: git stash apply stash@{0}" in output
|
||||
assert _git(repo, "status", "--porcelain").stdout.strip() == ""
|
||||
assert _git(repo, "stash", "list").stdout.strip(), "stash must be preserved"
|
||||
assert (repo / "tracked.txt").read_text(encoding="utf-8") == "upstream edit\n"
|
||||
|
||||
|
||||
@pytest.mark.live_system_guard_bypass
|
||||
@pytest.mark.skipif(
|
||||
shutil.which("git") is None or shutil.which("bash") is None,
|
||||
reason="needs git and bash",
|
||||
)
|
||||
def test_install_sh_repository_stage_recovers_from_autostash_conflict(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
managed = _make_conflicted_managed_checkout(tmp_path)
|
||||
env = os.environ | {
|
||||
"HERMES_HOME": str(tmp_path / "hermes-home"),
|
||||
"HERMES_INSTALL_DIR": str(managed),
|
||||
}
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", str(INSTALL_SH), "--stage", "repository", "--non-interactive"],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
_assert_conflict_was_recovered(managed, result.stdout)
|
||||
|
||||
|
||||
@pytest.mark.live_system_guard_bypass
|
||||
@pytest.mark.skipif(
|
||||
shutil.which("git") is None or POWERSHELL is None,
|
||||
reason="needs git and PowerShell",
|
||||
)
|
||||
def test_install_ps1_repository_stage_recovers_from_autostash_conflict(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
managed = _make_conflicted_managed_checkout(tmp_path)
|
||||
result = subprocess.run(
|
||||
[
|
||||
POWERSHELL,
|
||||
"-NoProfile",
|
||||
"-File",
|
||||
str(INSTALL_PS1),
|
||||
"-Stage",
|
||||
"repository",
|
||||
"-NonInteractive",
|
||||
"-InstallDir",
|
||||
str(managed),
|
||||
"-HermesHome",
|
||||
str(tmp_path / "hermes-home"),
|
||||
],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
_assert_conflict_was_recovered(managed, result.stdout)
|
||||
Loading…
Add table
Add a link
Reference in a new issue