"""Git operations: repo cloning and workspace management.""" from __future__ import annotations import asyncio import logging import tempfile from pathlib import Path log = logging.getLogger(__name__) def _validate_clone_args( owner: str, repo: str, workspace: Path, ) -> None: """Reject owner/repo values that could escape the workspace.""" for name, value in [("owner", owner), ("repo", repo)]: if "/" in value or ".." in value: msg = f"Invalid {name}: {value!r}" raise ValueError(msg) async def clone_repo( owner: str, repo: str, ref: str, token: str, workspace: Path, ) -> Path: """Shallow-clone a repo at the given ref into a temp directory.""" _validate_clone_args(owner, repo, workspace) # Ensure workspace exists before creating temp dir inside it. workspace.mkdir(parents=True, exist_ok=True) # Atomic unique directory -- avoids race conditions and rmtree. repo_dir = Path( tempfile.mkdtemp( prefix=f"{owner}_{repo}_", dir=workspace, ), ) if not str(repo_dir.resolve()).startswith( str(workspace.resolve()), ): msg = f"Path escapes workspace: {repo_dir}" raise ValueError(msg) clone_url = f"https://x-access-token:{token}@github.com/{owner}/{repo}.git" proc = await asyncio.create_subprocess_exec( "git", "clone", "--depth=1", "--branch", ref, clone_url, str(repo_dir), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) _, stderr = await proc.communicate() if proc.returncode != 0: log.error( "git clone failed (rc=%d): %s", proc.returncode, stderr.decode(), ) msg = f"git clone failed for {owner}/{repo} ref={ref}" raise RuntimeError(msg) return repo_dir async def commit_and_push( repo_dir: Path, branch: str, owner: str, repo: str, message: str = "codeflash-agent: optimize code", ) -> bool: """Stage all changes, commit, and push back to the PR branch. Returns ``True`` if changes were committed and pushed. """ # Stage everything proc = await asyncio.create_subprocess_exec( "git", "add", "-A", cwd=str(repo_dir), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) await proc.communicate() # Check if there are staged changes proc = await asyncio.create_subprocess_exec( "git", "diff", "--cached", "--quiet", cwd=str(repo_dir), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) await proc.communicate() if proc.returncode == 0: log.info("No changes to commit in %s", repo_dir) return False # Commit proc = await asyncio.create_subprocess_exec( "git", "commit", "-m", message, cwd=str(repo_dir), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) _, stderr = await proc.communicate() if proc.returncode != 0: log.error("git commit failed: %s", stderr.decode()) return False # Swap remote URL to use gh credential helper instead of the # installation token (which may lack push permission). plain_url = f"https://github.com/{owner}/{repo}.git" proc = await asyncio.create_subprocess_exec( "git", "remote", "set-url", "origin", plain_url, cwd=str(repo_dir), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) await proc.communicate() # Push to the PR branch proc = await asyncio.create_subprocess_exec( "git", "push", "origin", f"HEAD:{branch}", cwd=str(repo_dir), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) _, stderr = await proc.communicate() if proc.returncode != 0: log.error("git push failed: %s", stderr.decode()) return False log.info("Pushed optimization commit to %s", branch) return True