AgentTideDemo / git_utils.py
github-actions[bot]
Deploy Agent Tide Demo to HF Space with LFS
aded991
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