2026-03-20 14:38:07 +09:00
|
|
|
import asyncio
|
|
|
|
|
import logging
|
2026-03-20 17:44:45 +09:00
|
|
|
import os
|
2026-03-20 14:38:07 +09:00
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from langgraph.config import get_config
|
|
|
|
|
|
2026-03-20 17:44:45 +09:00
|
|
|
from agent.utils.gitea_client import get_gitea_client
|
|
|
|
|
|
2026-03-20 15:11:36 +09:00
|
|
|
from ..utils.git_utils import (
|
2026-03-20 14:38:07 +09:00
|
|
|
git_add_all,
|
|
|
|
|
git_checkout_branch,
|
|
|
|
|
git_commit,
|
|
|
|
|
git_config_user,
|
|
|
|
|
git_current_branch,
|
|
|
|
|
git_fetch_origin,
|
|
|
|
|
git_has_uncommitted_changes,
|
|
|
|
|
git_has_unpushed_commits,
|
|
|
|
|
git_push,
|
|
|
|
|
)
|
|
|
|
|
from ..utils.sandbox_paths import resolve_repo_dir
|
|
|
|
|
from ..utils.sandbox_state import get_sandbox_backend_sync
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def commit_and_open_pr(
|
|
|
|
|
title: str,
|
|
|
|
|
body: str,
|
|
|
|
|
commit_message: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
2026-03-20 15:11:36 +09:00
|
|
|
"""Commit all current changes and open a Gitea Pull Request.
|
2026-03-20 14:38:07 +09:00
|
|
|
|
|
|
|
|
Args:
|
2026-03-20 15:11:36 +09:00
|
|
|
title: PR title (under 70 characters)
|
|
|
|
|
body: PR description with ## Description and ## Test Plan
|
2026-03-20 14:38:07 +09:00
|
|
|
commit_message: Optional git commit message. If not provided, the PR title is used.
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-03-20 15:11:36 +09:00
|
|
|
Dictionary with success, error, pr_url, and pr_existing keys.
|
2026-03-20 14:38:07 +09:00
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
config = get_config()
|
|
|
|
|
configurable = config.get("configurable", {})
|
|
|
|
|
thread_id = configurable.get("thread_id")
|
|
|
|
|
|
|
|
|
|
if not thread_id:
|
|
|
|
|
return {"success": False, "error": "Missing thread_id in config", "pr_url": None}
|
|
|
|
|
|
|
|
|
|
repo_config = configurable.get("repo", {})
|
|
|
|
|
repo_owner = repo_config.get("owner")
|
|
|
|
|
repo_name = repo_config.get("name")
|
|
|
|
|
if not repo_owner or not repo_name:
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": "Missing repo owner/name in config",
|
|
|
|
|
"pr_url": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sandbox_backend = get_sandbox_backend_sync(thread_id)
|
|
|
|
|
if not sandbox_backend:
|
|
|
|
|
return {"success": False, "error": "No sandbox found for thread", "pr_url": None}
|
|
|
|
|
|
|
|
|
|
repo_dir = resolve_repo_dir(sandbox_backend, repo_name)
|
|
|
|
|
|
|
|
|
|
has_uncommitted_changes = git_has_uncommitted_changes(sandbox_backend, repo_dir)
|
|
|
|
|
git_fetch_origin(sandbox_backend, repo_dir)
|
|
|
|
|
has_unpushed_commits = git_has_unpushed_commits(sandbox_backend, repo_dir)
|
|
|
|
|
|
|
|
|
|
if not (has_uncommitted_changes or has_unpushed_commits):
|
|
|
|
|
return {"success": False, "error": "No changes detected", "pr_url": None}
|
|
|
|
|
|
|
|
|
|
current_branch = git_current_branch(sandbox_backend, repo_dir)
|
2026-03-20 15:11:36 +09:00
|
|
|
target_branch = f"galaxis-agent/{thread_id}"
|
2026-03-20 14:38:07 +09:00
|
|
|
if current_branch != target_branch:
|
|
|
|
|
if not git_checkout_branch(sandbox_backend, repo_dir, target_branch):
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": f"Failed to checkout branch {target_branch}",
|
|
|
|
|
"pr_url": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
git_config_user(
|
|
|
|
|
sandbox_backend,
|
|
|
|
|
repo_dir,
|
2026-03-20 15:11:36 +09:00
|
|
|
"galaxis-agent[bot]",
|
|
|
|
|
"galaxis-agent@users.noreply.gitea.local",
|
2026-03-20 14:38:07 +09:00
|
|
|
)
|
|
|
|
|
git_add_all(sandbox_backend, repo_dir)
|
|
|
|
|
|
|
|
|
|
commit_msg = commit_message or title
|
|
|
|
|
if has_uncommitted_changes:
|
|
|
|
|
commit_result = git_commit(sandbox_backend, repo_dir, commit_msg)
|
|
|
|
|
if commit_result.exit_code != 0:
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": f"Git commit failed: {commit_result.output.strip()}",
|
|
|
|
|
"pr_url": None,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 15:11:36 +09:00
|
|
|
gitea_token = os.environ.get("GITEA_TOKEN", "")
|
|
|
|
|
if not gitea_token:
|
|
|
|
|
logger.error("commit_and_open_pr missing Gitea token for thread %s", thread_id)
|
2026-03-20 14:38:07 +09:00
|
|
|
return {
|
|
|
|
|
"success": False,
|
2026-03-20 15:11:36 +09:00
|
|
|
"error": "Missing Gitea token",
|
2026-03-20 14:38:07 +09:00
|
|
|
"pr_url": None,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 15:11:36 +09:00
|
|
|
push_result = git_push(sandbox_backend, repo_dir, target_branch, gitea_token)
|
2026-03-20 14:38:07 +09:00
|
|
|
if push_result.exit_code != 0:
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": f"Git push failed: {push_result.output.strip()}",
|
|
|
|
|
"pr_url": None,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:44:45 +09:00
|
|
|
# --- PR 생성 (GiteaClient) ---
|
|
|
|
|
gitea_external_url = os.environ.get("GITEA_EXTERNAL_URL", "")
|
|
|
|
|
gitea_internal_url = os.environ.get("GITEA_URL", "http://gitea:3000")
|
|
|
|
|
default_branch = os.environ.get("DEFAULT_BRANCH", "main")
|
|
|
|
|
client = get_gitea_client()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
pr_result = asyncio.run(
|
|
|
|
|
client.create_pull_request(
|
|
|
|
|
owner=repo_owner,
|
|
|
|
|
repo=repo_name,
|
|
|
|
|
title=title,
|
|
|
|
|
head=target_branch,
|
|
|
|
|
base=default_branch,
|
|
|
|
|
body=body,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
pr_url = pr_result.get("html_url", "")
|
|
|
|
|
if gitea_external_url and pr_url:
|
|
|
|
|
pr_url = pr_url.replace(gitea_internal_url, gitea_external_url)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"pr_url": pr_url,
|
|
|
|
|
"pr_number": pr_result.get("number"),
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.exception("Failed to create PR (push succeeded)")
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"pr_url": "",
|
|
|
|
|
"error": f"Push succeeded but PR creation failed: {e}",
|
|
|
|
|
}
|
2026-03-20 14:38:07 +09:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.exception("commit_and_open_pr failed")
|
|
|
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}", "pr_url": None}
|