mirror of
https://github.com/codeflash-ai/codeflash-agent.git
synced 2026-05-04 18:25:19 +00:00
* 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.
159 lines
4.1 KiB
Python
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
|