codeflash-agent/packages/github-app/github_app/git.py
Kevin Turcios 2caaf6af7c
Fix CI: mypy errors, ruff formatting, switch to prek (#22)
* Fix mypy errors and apply ruff formatting across packages

Fix ast.FunctionDef calls missing type_params for Python 3.12+,
correct type: ignore error codes in _comparator and _plugin, and
run ruff format on all package source and test files.

* Switch CI to prek for lint/typecheck checks

Use j178/prek-action for consistent lint+typecheck (ruff check,
ruff format, interrogate, mypy) matching local pre-commit config.
Keep test as a separate parallel job for test-env support.
2026-04-15 02:52:47 -05:00

159 lines
4.1 KiB
Python

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