Spaces:
Running
Running
| 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 |