"""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