mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 07:52:14 +00:00
Merge pull request #950 from langchain-ai/yogesh/repo-clone-fix
fix: clone error when repo directory already exists
This commit is contained in:
commit
ffabcea6c4
2 changed files with 131 additions and 88 deletions
|
|
@ -46,6 +46,11 @@ SANDBOX_CREATION_TIMEOUT = 180
|
||||||
SANDBOX_POLL_INTERVAL = 1.0
|
SANDBOX_POLL_INTERVAL = 1.0
|
||||||
|
|
||||||
from .utils.sandbox_state import SANDBOX_BACKENDS
|
from .utils.sandbox_state import SANDBOX_BACKENDS
|
||||||
|
from .utils.github import (
|
||||||
|
git_has_uncommitted_changes,
|
||||||
|
is_valid_git_repo,
|
||||||
|
remove_directory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915
|
async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915
|
||||||
|
|
@ -75,89 +80,96 @@ async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
repo_dir = f"/workspace/{repo}"
|
repo_dir = f"/workspace/{repo}"
|
||||||
|
auth_url = f"https://git:{token}@github.com/{owner}/{repo}.git"
|
||||||
|
clean_url = f"https://github.com/{owner}/{repo}.git"
|
||||||
|
|
||||||
logger.debug("Checking if repo already exists at %s", repo_dir)
|
is_git_repo = await loop.run_in_executor(None, is_valid_git_repo, sandbox_backend, repo_dir)
|
||||||
|
|
||||||
|
if not is_git_repo:
|
||||||
|
logger.warning(
|
||||||
|
"Repo directory missing or not a valid git repo at %s, removing", repo_dir
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
removed = await loop.run_in_executor(None, remove_directory, sandbox_backend, repo_dir)
|
||||||
|
if not removed:
|
||||||
|
msg = f"Failed to remove invalid directory at {repo_dir}"
|
||||||
|
logger.error(msg)
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
logger.info("Removed invalid directory, will clone fresh repo")
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to remove invalid directory")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
logger.info("Repo exists at %s, checking for uncommitted changes", repo_dir)
|
||||||
|
has_changes = await loop.run_in_executor(
|
||||||
|
None, git_has_uncommitted_changes, sandbox_backend, repo_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_changes:
|
||||||
|
logger.warning("Repo has uncommitted changes at %s, skipping pull", repo_dir)
|
||||||
|
return repo_dir
|
||||||
|
|
||||||
|
logger.info("Repo is clean, pulling latest changes from %s/%s", owner, repo)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
sandbox_backend.execute,
|
||||||
|
f"cd {repo_dir} && git remote set-url origin {auth_url}",
|
||||||
|
)
|
||||||
|
pull_result = await loop.run_in_executor(
|
||||||
|
None, sandbox_backend.execute, f"cd {repo_dir} && git pull origin"
|
||||||
|
)
|
||||||
|
logger.debug("Git pull result: exit_code=%s", pull_result.exit_code)
|
||||||
|
if pull_result.exit_code != 0:
|
||||||
|
logger.warning(
|
||||||
|
"Git pull failed with exit code %s: %s",
|
||||||
|
pull_result.exit_code,
|
||||||
|
pull_result.output[:200] if pull_result.output else "",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to execute git pull")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
sandbox_backend.execute,
|
||||||
|
f"cd {repo_dir} && git remote set-url origin {clean_url}",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to restore clean remote URL")
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info("Repo updated at %s", repo_dir)
|
||||||
|
return repo_dir
|
||||||
|
|
||||||
|
logger.info("Cloning repo %s/%s to %s", owner, repo, repo_dir)
|
||||||
try:
|
try:
|
||||||
check_result = await loop.run_in_executor(
|
result = await loop.run_in_executor(
|
||||||
None, sandbox_backend.execute, f"test -d {repo_dir}/.git && echo exists"
|
None, sandbox_backend.execute, f"git clone {auth_url} {repo_dir}"
|
||||||
)
|
|
||||||
logger.debug(
|
|
||||||
"Check result: exit_code=%s, output=%s",
|
|
||||||
check_result.exit_code,
|
|
||||||
check_result.output[:200] if check_result.output else "",
|
|
||||||
)
|
)
|
||||||
|
logger.debug("Git clone result: exit_code=%s", result.exit_code)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to execute check command in sandbox")
|
logger.exception("Failed to execute git clone")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if check_result.exit_code == 0 and "exists" in check_result.output:
|
if result.exit_code != 0:
|
||||||
logger.info("Repo already exists at %s, pulling latest changes", repo_dir)
|
msg = f"Failed to clone repo {owner}/{repo}: {result.output}"
|
||||||
try:
|
logger.error(msg)
|
||||||
status_result = await loop.run_in_executor(
|
raise RuntimeError(msg)
|
||||||
None, sandbox_backend.execute, f"cd {repo_dir} && git status --porcelain"
|
|
||||||
)
|
|
||||||
logger.debug("Git status result: exit_code=%s", status_result.exit_code)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to get git status")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# CRITICAL: Ensure remote URL doesn't contain token (clean up from previous runs)
|
try:
|
||||||
clean_url = f"https://github.com/{owner}/{repo}.git"
|
await loop.run_in_executor(
|
||||||
try:
|
None,
|
||||||
await loop.run_in_executor(
|
sandbox_backend.execute,
|
||||||
None,
|
f"cd {repo_dir} && git remote set-url origin {clean_url}",
|
||||||
sandbox_backend.execute,
|
)
|
||||||
f"cd {repo_dir} && git remote set-url origin {clean_url}",
|
except Exception:
|
||||||
)
|
logger.exception("Failed to set remote URL after clone")
|
||||||
except Exception:
|
raise
|
||||||
logger.exception("Failed to set remote URL")
|
|
||||||
raise
|
|
||||||
|
|
||||||
if status_result.exit_code == 0 and not status_result.output.strip():
|
logger.info("Repo cloned successfully at %s", repo_dir)
|
||||||
auth_url = f"https://git:{token}@github.com/{owner}/{repo}.git"
|
|
||||||
try:
|
|
||||||
pull_result = await loop.run_in_executor(
|
|
||||||
None, sandbox_backend.execute, f"cd {repo_dir} && git pull {auth_url}"
|
|
||||||
)
|
|
||||||
logger.debug("Git pull result: exit_code=%s", pull_result.exit_code)
|
|
||||||
if pull_result.exit_code != 0:
|
|
||||||
logger.warning(
|
|
||||||
"Git pull failed with exit code %s: %s",
|
|
||||||
pull_result.exit_code,
|
|
||||||
pull_result.output[:200] if pull_result.output else "",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to execute git pull")
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
logger.info("Cloning repo %s/%s to %s", owner, repo, repo_dir)
|
|
||||||
clone_url = f"https://git:{token}@github.com/{owner}/{repo}.git"
|
|
||||||
try:
|
|
||||||
result = await loop.run_in_executor(
|
|
||||||
None, sandbox_backend.execute, f"git clone {clone_url} {repo_dir}"
|
|
||||||
)
|
|
||||||
logger.debug("Git clone result: exit_code=%s", result.exit_code)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to execute git clone")
|
|
||||||
raise
|
|
||||||
|
|
||||||
if result.exit_code != 0:
|
|
||||||
msg = f"Failed to clone repo {owner}/{repo}: {result.output}"
|
|
||||||
logger.error(msg)
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
|
|
||||||
clean_url = f"https://github.com/{owner}/{repo}.git"
|
|
||||||
try:
|
|
||||||
await loop.run_in_executor(
|
|
||||||
None,
|
|
||||||
sandbox_backend.execute,
|
|
||||||
f"cd {repo_dir} && git remote set-url origin {clean_url}",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to set remote URL after clone")
|
|
||||||
raise
|
|
||||||
|
|
||||||
logger.info("Repo setup complete at %s", repo_dir)
|
|
||||||
return repo_dir
|
return repo_dir
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
import shlex
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from deepagents.backends.protocol import ExecuteResponse, SandboxBackendProtocol
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -15,23 +15,46 @@ HTTP_CREATED = 201
|
||||||
HTTP_UNPROCESSABLE_ENTITY = 422
|
HTTP_UNPROCESSABLE_ENTITY = 422
|
||||||
|
|
||||||
|
|
||||||
def _run_git(sandbox_backend: Any, repo_dir: str, command: str) -> Any:
|
def _run_git(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str, command: str
|
||||||
|
) -> ExecuteResponse:
|
||||||
"""Run a git command in the sandbox repo directory."""
|
"""Run a git command in the sandbox repo directory."""
|
||||||
return sandbox_backend.execute(f"cd {repo_dir} && {command}")
|
return sandbox_backend.execute(f"cd {repo_dir} && {command}")
|
||||||
|
|
||||||
|
|
||||||
def git_has_uncommitted_changes(sandbox_backend: Any, repo_dir: str) -> bool:
|
def is_valid_git_repo(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool:
|
||||||
|
"""Check if directory is a valid git repository."""
|
||||||
|
git_dir = f"{repo_dir}/.git"
|
||||||
|
safe_git_dir = shlex.quote(git_dir)
|
||||||
|
result = sandbox_backend.execute(f"test -d {safe_git_dir} && echo exists")
|
||||||
|
return result.exit_code == 0 and "exists" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def remove_directory(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool:
|
||||||
|
"""Remove a directory and all its contents."""
|
||||||
|
safe_repo_dir = shlex.quote(repo_dir)
|
||||||
|
result = sandbox_backend.execute(f"rm -rf {safe_repo_dir}")
|
||||||
|
return result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def git_has_uncommitted_changes(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str
|
||||||
|
) -> bool:
|
||||||
"""Check whether the repo has uncommitted changes."""
|
"""Check whether the repo has uncommitted changes."""
|
||||||
result = _run_git(sandbox_backend, repo_dir, "git status --porcelain")
|
result = _run_git(sandbox_backend, repo_dir, "git status --porcelain")
|
||||||
return result.exit_code == 0 and bool(result.output.strip())
|
return result.exit_code == 0 and bool(result.output.strip())
|
||||||
|
|
||||||
|
|
||||||
def git_fetch_origin(sandbox_backend: Any, repo_dir: str) -> Any:
|
def git_fetch_origin(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str
|
||||||
|
) -> ExecuteResponse:
|
||||||
"""Fetch latest from origin (best-effort)."""
|
"""Fetch latest from origin (best-effort)."""
|
||||||
return _run_git(sandbox_backend, repo_dir, "git fetch origin 2>/dev/null || true")
|
return _run_git(sandbox_backend, repo_dir, "git fetch origin 2>/dev/null || true")
|
||||||
|
|
||||||
|
|
||||||
def git_has_unpushed_commits(sandbox_backend: Any, repo_dir: str) -> bool:
|
def git_has_unpushed_commits(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str
|
||||||
|
) -> bool:
|
||||||
"""Check whether there are commits not pushed to upstream."""
|
"""Check whether there are commits not pushed to upstream."""
|
||||||
git_log_cmd = (
|
git_log_cmd = (
|
||||||
"git log --oneline @{upstream}..HEAD 2>/dev/null "
|
"git log --oneline @{upstream}..HEAD 2>/dev/null "
|
||||||
|
|
@ -41,13 +64,15 @@ def git_has_unpushed_commits(sandbox_backend: Any, repo_dir: str) -> bool:
|
||||||
return result.exit_code == 0 and bool(result.output.strip())
|
return result.exit_code == 0 and bool(result.output.strip())
|
||||||
|
|
||||||
|
|
||||||
def git_current_branch(sandbox_backend: Any, repo_dir: str) -> str:
|
def git_current_branch(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> str:
|
||||||
"""Get the current git branch name."""
|
"""Get the current git branch name."""
|
||||||
result = _run_git(sandbox_backend, repo_dir, "git rev-parse --abbrev-ref HEAD")
|
result = _run_git(sandbox_backend, repo_dir, "git rev-parse --abbrev-ref HEAD")
|
||||||
return result.output.strip() if result.exit_code == 0 else ""
|
return result.output.strip() if result.exit_code == 0 else ""
|
||||||
|
|
||||||
|
|
||||||
def git_checkout_branch(sandbox_backend: Any, repo_dir: str, branch: str) -> bool:
|
def git_checkout_branch(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str, branch: str
|
||||||
|
) -> bool:
|
||||||
"""Checkout branch, creating it if needed."""
|
"""Checkout branch, creating it if needed."""
|
||||||
safe_branch = shlex.quote(branch)
|
safe_branch = shlex.quote(branch)
|
||||||
checkout_result = _run_git(
|
checkout_result = _run_git(
|
||||||
|
|
@ -60,7 +85,7 @@ def git_checkout_branch(sandbox_backend: Any, repo_dir: str, branch: str) -> boo
|
||||||
|
|
||||||
|
|
||||||
def git_config_user(
|
def git_config_user(
|
||||||
sandbox_backend: Any,
|
sandbox_backend: SandboxBackendProtocol,
|
||||||
repo_dir: str,
|
repo_dir: str,
|
||||||
name: str,
|
name: str,
|
||||||
email: str,
|
email: str,
|
||||||
|
|
@ -72,18 +97,24 @@ def git_config_user(
|
||||||
_run_git(sandbox_backend, repo_dir, f"git config user.email {safe_email}")
|
_run_git(sandbox_backend, repo_dir, f"git config user.email {safe_email}")
|
||||||
|
|
||||||
|
|
||||||
def git_add_all(sandbox_backend: Any, repo_dir: str) -> Any:
|
def git_add_all(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str
|
||||||
|
) -> ExecuteResponse:
|
||||||
"""Stage all changes."""
|
"""Stage all changes."""
|
||||||
return _run_git(sandbox_backend, repo_dir, "git add -A")
|
return _run_git(sandbox_backend, repo_dir, "git add -A")
|
||||||
|
|
||||||
|
|
||||||
def git_commit(sandbox_backend: Any, repo_dir: str, message: str) -> Any:
|
def git_commit(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str, message: str
|
||||||
|
) -> ExecuteResponse:
|
||||||
"""Commit staged changes with the given message."""
|
"""Commit staged changes with the given message."""
|
||||||
safe_message = shlex.quote(message)
|
safe_message = shlex.quote(message)
|
||||||
return _run_git(sandbox_backend, repo_dir, f"git commit -m {safe_message}")
|
return _run_git(sandbox_backend, repo_dir, f"git commit -m {safe_message}")
|
||||||
|
|
||||||
|
|
||||||
def git_get_remote_url(sandbox_backend: Any, repo_dir: str) -> str | None:
|
def git_get_remote_url(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, repo_dir: str
|
||||||
|
) -> str | None:
|
||||||
"""Get the origin remote URL."""
|
"""Get the origin remote URL."""
|
||||||
result = _run_git(sandbox_backend, repo_dir, "git remote get-url origin")
|
result = _run_git(sandbox_backend, repo_dir, "git remote get-url origin")
|
||||||
if result.exit_code != 0:
|
if result.exit_code != 0:
|
||||||
|
|
@ -92,11 +123,11 @@ def git_get_remote_url(sandbox_backend: Any, repo_dir: str) -> str | None:
|
||||||
|
|
||||||
|
|
||||||
def git_push(
|
def git_push(
|
||||||
sandbox_backend: Any,
|
sandbox_backend: SandboxBackendProtocol,
|
||||||
repo_dir: str,
|
repo_dir: str,
|
||||||
branch: str,
|
branch: str,
|
||||||
github_token: str | None = None,
|
github_token: str | None = None,
|
||||||
) -> Any:
|
) -> ExecuteResponse:
|
||||||
"""Push the branch to origin, using a token if needed."""
|
"""Push the branch to origin, using a token if needed."""
|
||||||
safe_branch = shlex.quote(branch)
|
safe_branch = shlex.quote(branch)
|
||||||
remote_url = git_get_remote_url(sandbox_backend, repo_dir)
|
remote_url = git_get_remote_url(sandbox_backend, repo_dir)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue