317 lines
9.0 KiB
Python
317 lines
9.0 KiB
Python
"""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}")
|