auto-swe-agent-ui / tools /git_tools.py
DevilBits's picture
fix: enforce safe empty bounds for tracking data charts and match dataframe list alignments
6085b61
"""Git tools for auto-swe-agent. All commands run inside the Docker sandbox."""
import re
from typing import List, Optional
from langchain_core.tools import tool
def _run_in_sandbox(command: str, workspace_dir: str = "./") -> tuple[int, str]:
"""Run a shell command in the Docker sandbox, return (exit_code, output)."""
# Import here to avoid circular imports with agent.py
from agent import get_sandbox
container = get_sandbox(workspace_dir)
result = container.exec_run(
["bash", "-c", command], workdir="/workspace", demux=False
)
output = (result.output or b"").decode().strip()
return result.exit_code, output
@tool
def create_branch(branch_name: str, workspace_dir: str = "./") -> str:
"""Create and checkout a new git branch in the Docker sandbox.
Branch name must be lowercase with hyphens only (no spaces or special chars).
"""
# Validate branch name
if not re.match(r"^[a-z0-9/][a-z0-9\-/]*$", branch_name):
return f"Error: invalid branch name '{branch_name}'. Use lowercase letters, numbers, hyphens, and forward slashes only."
exit_code, output = _run_in_sandbox(f"git checkout -b {branch_name}", workspace_dir)
if exit_code != 0:
return f"Error creating branch: {output}"
return f"Created and checked out branch: {branch_name}"
@tool
def commit_changes(
message: str, workspace_dir: str = "./", files: Optional[List[str]] = None
) -> str:
"""Stage and commit changes in the Docker sandbox.
If files is provided, only those files are staged; otherwise all changes are staged.
Commit message is auto-prefixed with 'auto-swe: ' if not already present.
"""
if not message.startswith("auto-swe:"):
message = f"auto-swe: {message}"
# Configure git identity inside container (required for commits)
_run_in_sandbox(
'git config user.email "agent@auto-swe-agent" && git config user.name "auto-swe-agent"',
workspace_dir,
)
if files:
file_list = " ".join(f'"{f}"' for f in files)
add_cmd = f"git add {file_list}"
else:
add_cmd = "git add -A"
exit_code, output = _run_in_sandbox(add_cmd, workspace_dir)
if exit_code != 0:
return f"Error staging files: {output}"
exit_code, output = _run_in_sandbox(f'git commit -m "{message}"', workspace_dir)
if exit_code != 0:
return f"Error committing: {output}"
# Extract commit hash from output
commit_hash = ""
for line in output.splitlines():
if line.startswith("["):
# e.g. "[main abc1234] auto-swe: fix something"
parts = line.split()
if len(parts) >= 2:
commit_hash = parts[1].rstrip("]")
break
return f"Committed: {message}\nHash: {commit_hash}\n{output}"
@tool
def generate_pr_description(workspace_dir: str = "./") -> str:
"""Generate a pull request description based on the latest git diff.
Uses git diff HEAD~1 if commits exist, otherwise git diff for staged changes.
"""
# Try diff against previous commit first
exit_code, diff = _run_in_sandbox(
"git diff HEAD~1 --stat 2>/dev/null || git diff --stat", workspace_dir
)
_, full_diff = _run_in_sandbox(
"git diff HEAD~1 2>/dev/null || git diff", workspace_dir
)
# Truncate diff for context window
if len(full_diff) > 3000:
full_diff = full_diff[:3000] + "\n... [diff truncated]"
# Parse changed files from stat output
changed_files = []
for line in diff.splitlines():
line = line.strip()
if "|" in line and not line.startswith("---"):
fname = line.split("|")[0].strip()
if fname:
changed_files.append(fname)
files_section = (
"\n".join(f"- `{f}`" for f in changed_files)
if changed_files
else "- (see diff)"
)
pr_description = f"""## Changes
{files_section}
## Testing
- pytest passed ✓
## Diff Summary
```
{full_diff}
```
---
*Generated by auto-swe-agent*"""
return pr_description