Spaces:
Runtime error
Runtime error
from pathlib import Path | |
from ulid import ulid | |
import subprocess | |
import asyncio | |
import pygit2 | |
import re | |
GIT_URL_PATTERN = re.compile( | |
r'^(?:http|https|git|ssh)://' # Protocol | |
r'(?:\S+@)?' # Optional username | |
r'([^/]+)' # Domain | |
r'(?:[:/])([^/]+/[^/]+?)(?:\.git)?$' # Repo path | |
) | |
async def validate_git_url(url) -> None: | |
"""Validate the Git repository URL using git ls-remote.""" | |
if not GIT_URL_PATTERN.match(url): | |
raise ValueError(f"Invalid Git repository URL format: {url}") | |
try: | |
process = await asyncio.create_subprocess_exec( | |
"git", "ls-remote", url, | |
stdout=asyncio.subprocess.PIPE, | |
stderr=asyncio.subprocess.PIPE | |
) | |
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10) | |
if process.returncode != 0: | |
raise subprocess.CalledProcessError(process.returncode, ["git", "ls-remote", url], stdout, stderr) | |
if not stdout.strip(): | |
raise ValueError(f"URL {url} points to an empty repository") | |
except asyncio.TimeoutError: | |
process.kill() | |
await process.wait() | |
raise ValueError(f"Timeout while validating URL {url}") | |
except subprocess.CalledProcessError as e: | |
raise ValueError(f"Invalid Git repository URL: {url}. Error: {e.stderr}") from e | |
async def commit_and_push_changes(repo_path: Path, branch_name: str = None, commit_message: str = "Auto-commit: Save changes", checkout :bool=True) -> None: | |
"""Add all changes, commit with default message, and push to remote.""" | |
repo_path_str = str(repo_path) | |
try: | |
# Create new branch with Agent Tide + ULID name if not provided | |
if not branch_name: | |
branch_name = f"agent-tide-{ulid()}" | |
if checkout: | |
# Create and checkout new branch | |
process = await asyncio.create_subprocess_exec( | |
"git", "checkout", "-b", branch_name, | |
cwd=repo_path_str, | |
stdout=asyncio.subprocess.PIPE, | |
stderr=asyncio.subprocess.PIPE | |
) | |
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10) | |
if process.returncode != 0: | |
raise subprocess.CalledProcessError(process.returncode, ["git", "checkout", "-b", branch_name], stdout, stderr) | |
# Add all changes | |
process = await asyncio.create_subprocess_exec( | |
"git", "add", ".", | |
cwd=repo_path_str, | |
stdout=asyncio.subprocess.PIPE, | |
stderr=asyncio.subprocess.PIPE | |
) | |
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30) | |
if process.returncode != 0: | |
raise subprocess.CalledProcessError(process.returncode, ["git", "add", "."], stdout, stderr) | |
# Commit changes | |
process = await asyncio.create_subprocess_exec( | |
"git", "commit", "-m", commit_message, | |
cwd=repo_path_str, | |
stdout=asyncio.subprocess.PIPE, | |
stderr=asyncio.subprocess.PIPE | |
) | |
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30) | |
if process.returncode != 0: | |
# Check if it's because there are no changes to commit | |
if "nothing to commit" in stderr or "nothing to commit" in stdout: | |
return # No changes to commit, exit gracefully | |
raise subprocess.CalledProcessError(process.returncode, ["git", "commit", "-m", commit_message], stdout, stderr) | |
# Push to remote | |
process = await asyncio.create_subprocess_exec( | |
"git", "push", "origin", branch_name, | |
cwd=repo_path_str, | |
stdout=asyncio.subprocess.PIPE, | |
stderr=asyncio.subprocess.PIPE | |
) | |
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=60) | |
if process.returncode != 0: | |
raise subprocess.CalledProcessError(process.returncode, ["git", "push", "origin", branch_name], stdout, stderr) | |
except asyncio.TimeoutError: | |
process.kill() | |
await process.wait() | |
raise ValueError(f"Timeout during git operation in {repo_path}") | |
except subprocess.CalledProcessError as e: | |
raise ValueError(f"Git operation failed in {repo_path}. Error: {e.stderr}") from e | |
def push_new_branch(repo :pygit2.Repository, branch_name :str, remote_name :str='origin'): | |
""" | |
Push a new branch to remote origin (equivalent to 'git push origin branch_name') | |
Args: | |
repo (pygit2.Repository): Repo Obj | |
branch_name (str): Name of the branch to push | |
remote_name (str): Name of the remote (default: 'origin') | |
Returns: | |
bool: True if push was successful, False otherwise | |
""" | |
# Get the remote | |
remote = repo.remotes[remote_name] | |
# Create refspec for pushing new branch | |
# Format: local_branch:remote_branch (this publishes the new branch) | |
refspec = f'refs/heads/{branch_name}:refs/heads/{branch_name}' | |
# Push to remote | |
result = remote.push([refspec]) | |
# Check if push was successful (no error message means success) | |
return not result.error_message | |
def checkout_new_branch(repo :pygit2.Repository, new_branch_name :str, start_point=None): | |
""" | |
Create and checkout a new branch from the current HEAD or specified start point. | |
Args: | |
repo_path (str): Path to the git repository | |
new_branch_name (str): Name of the new branch to create and checkout | |
start_point (pygit2.Oid or Reference, optional): Commit or reference to start from. | |
If None, uses current HEAD. | |
Returns: | |
pygit2.Reference: The newly created branch reference | |
Raises: | |
ValueError: If branch already exists or invalid start point | |
Exception: For other git-related errors | |
""" | |
# Check if branch already exists | |
if new_branch_name in repo.branches.local: | |
raise ValueError(f"Branch '{new_branch_name}' already exists") | |
# Get the start point commit (default to HEAD) | |
if start_point is None: | |
if repo.head_is_detached: | |
raise ValueError("HEAD is detached, please specify a start point") | |
start_point = repo.head.target | |
# Create the new branch | |
new_branch = repo.branches.local.create(new_branch_name, repo[start_point]) | |
# Checkout the new branch | |
repo.checkout(new_branch, strategy=pygit2.GIT_CHECKOUT_SAFE) | |
return new_branch |