Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:28:42 +08:00
commit 8a4be47b6e
43 changed files with 10867 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
"""Git operations for ADW composable architecture.
Provides centralized git operations that build on top of github.py module.
"""
import subprocess
import json
import logging
from typing import Optional, Tuple
# Import GitHub functions from existing module
from adw_modules.github import get_repo_url, extract_repo_path, make_issue_comment
def get_current_branch(cwd: Optional[str] = None) -> str:
"""Get current git branch name."""
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True,
text=True,
cwd=cwd,
)
return result.stdout.strip()
def push_branch(
branch_name: str, cwd: Optional[str] = None
) -> Tuple[bool, Optional[str]]:
"""Push current branch to remote. Returns (success, error_message)."""
result = subprocess.run(
["git", "push", "-u", "origin", branch_name],
capture_output=True,
text=True,
cwd=cwd,
)
if result.returncode != 0:
return False, result.stderr
return True, None
def check_pr_exists(branch_name: str) -> Optional[str]:
"""Check if PR exists for branch. Returns PR URL if exists."""
# Use github.py functions to get repo info
try:
repo_url = get_repo_url()
repo_path = extract_repo_path(repo_url)
except Exception as e:
return None
result = subprocess.run(
[
"gh",
"pr",
"list",
"--repo",
repo_path,
"--head",
branch_name,
"--json",
"url",
],
capture_output=True,
text=True,
)
if result.returncode == 0:
prs = json.loads(result.stdout)
if prs:
return prs[0]["url"]
return None
def create_branch(
branch_name: str, cwd: Optional[str] = None
) -> Tuple[bool, Optional[str]]:
"""Create and checkout a new branch. Returns (success, error_message)."""
# Create branch
result = subprocess.run(
["git", "checkout", "-b", branch_name], capture_output=True, text=True, cwd=cwd
)
if result.returncode != 0:
# Check if error is because branch already exists
if "already exists" in result.stderr:
# Try to checkout existing branch
result = subprocess.run(
["git", "checkout", branch_name],
capture_output=True,
text=True,
cwd=cwd,
)
if result.returncode != 0:
return False, result.stderr
return True, None
return False, result.stderr
return True, None
def commit_changes(
message: str, cwd: Optional[str] = None
) -> Tuple[bool, Optional[str]]:
"""Stage all changes and commit. Returns (success, error_message)."""
# Check if there are changes to commit
result = subprocess.run(
["git", "status", "--porcelain"], capture_output=True, text=True, cwd=cwd
)
if not result.stdout.strip():
return True, None # No changes to commit
# Stage all changes
result = subprocess.run(
["git", "add", "-A"], capture_output=True, text=True, cwd=cwd
)
if result.returncode != 0:
return False, result.stderr
# Commit
result = subprocess.run(
["git", "commit", "-m", message], capture_output=True, text=True, cwd=cwd
)
if result.returncode != 0:
return False, result.stderr
return True, None
def get_pr_number(branch_name: str) -> Optional[str]:
"""Get PR number for a branch. Returns PR number if exists."""
# Use github.py functions to get repo info
try:
repo_url = get_repo_url()
repo_path = extract_repo_path(repo_url)
except Exception as e:
return None
result = subprocess.run(
[
"gh",
"pr",
"list",
"--repo",
repo_path,
"--head",
branch_name,
"--json",
"number",
"--limit",
"1",
],
capture_output=True,
text=True,
)
if result.returncode == 0:
prs = json.loads(result.stdout)
if prs:
return str(prs[0]["number"])
return None
def approve_pr(pr_number: str, logger: logging.Logger) -> Tuple[bool, Optional[str]]:
"""Approve a PR. Returns (success, error_message)."""
try:
repo_url = get_repo_url()
repo_path = extract_repo_path(repo_url)
except Exception as e:
return False, f"Failed to get repo info: {e}"
result = subprocess.run(
[
"gh",
"pr",
"review",
pr_number,
"--repo",
repo_path,
"--approve",
"--body",
"ADW Ship workflow approved this PR after validating all state fields.",
],
capture_output=True,
text=True,
)
if result.returncode != 0:
return False, result.stderr
logger.info(f"Approved PR #{pr_number}")
return True, None
def merge_pr(
pr_number: str, logger: logging.Logger, merge_method: str = "squash"
) -> Tuple[bool, Optional[str]]:
"""Merge a PR. Returns (success, error_message).
Args:
pr_number: The PR number to merge
logger: Logger instance
merge_method: One of 'merge', 'squash', 'rebase' (default: 'squash')
"""
try:
repo_url = get_repo_url()
repo_path = extract_repo_path(repo_url)
except Exception as e:
return False, f"Failed to get repo info: {e}"
# First check if PR is mergeable
result = subprocess.run(
[
"gh",
"pr",
"view",
pr_number,
"--repo",
repo_path,
"--json",
"mergeable,mergeStateStatus",
],
capture_output=True,
text=True,
)
if result.returncode != 0:
return False, f"Failed to check PR status: {result.stderr}"
pr_status = json.loads(result.stdout)
if pr_status.get("mergeable") != "MERGEABLE":
return (
False,
f"PR is not mergeable. Status: {pr_status.get('mergeStateStatus', 'unknown')}",
)
# Merge the PR
merge_cmd = [
"gh",
"pr",
"merge",
pr_number,
"--repo",
repo_path,
f"--{merge_method}",
]
# Add auto-merge body
merge_cmd.extend(
["--body", "Merged by ADW Ship workflow after successful validation."]
)
result = subprocess.run(merge_cmd, capture_output=True, text=True)
if result.returncode != 0:
return False, result.stderr
logger.info(f"Merged PR #{pr_number} using {merge_method} method")
return True, None
def finalize_git_operations(
state: "ADWState", logger: logging.Logger, cwd: Optional[str] = None
) -> None:
"""Standard git finalization: push branch and create/update PR."""
branch_name = state.get("branch_name")
if not branch_name:
# Fallback: use current git branch if not main
current_branch = get_current_branch(cwd=cwd)
if current_branch and current_branch != "main":
logger.warning(
f"No branch name in state, using current branch: {current_branch}"
)
branch_name = current_branch
else:
logger.error(
"No branch name in state and current branch is main, skipping git operations"
)
return
# Always push
success, error = push_branch(branch_name, cwd=cwd)
if not success:
logger.error(f"Failed to push branch: {error}")
return
logger.info(f"Pushed branch: {branch_name}")
# Handle PR
pr_url = check_pr_exists(branch_name)
issue_number = state.get("issue_number")
adw_id = state.get("adw_id")
if pr_url:
logger.info(f"Found existing PR: {pr_url}")
# Post PR link for easy reference
if issue_number and adw_id:
make_issue_comment(issue_number, f"{adw_id}_ops: ✅ Pull request: {pr_url}")
else:
# Create new PR - fetch issue data first
if issue_number:
try:
repo_url = get_repo_url()
repo_path = extract_repo_path(repo_url)
from adw_modules.github import fetch_issue
issue = fetch_issue(issue_number, repo_path)
from adw_modules.workflow_ops import create_pull_request
pr_url, error = create_pull_request(branch_name, issue, state, logger, cwd)
except Exception as e:
logger.error(f"Failed to fetch issue for PR creation: {e}")
pr_url, error = None, str(e)
else:
pr_url, error = None, "No issue number in state"
if pr_url:
logger.info(f"Created PR: {pr_url}")
# Post new PR link
if issue_number and adw_id:
make_issue_comment(
issue_number, f"{adw_id}_ops: ✅ Pull request created: {pr_url}"
)
else:
logger.error(f"Failed to create PR: {error}")