Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions exercise_utils/github_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,119 @@ def get_remote_url(repository_name: str, verbose: bool) -> str:
remote_url = f"git@github.com:{repository_name}.git"

return remote_url


def create_pr(title: str, body: str, base: str, head: str, verbose: bool) -> bool:
"""Create a pull request."""
command = [
"gh",
"pr",
"create",
"--title",
title,
"--body",
body,
"--base",
base,
"--head",
head,
]

result = run(command, verbose)
return result.is_success()


def view_pr(pr_number: int, verbose: bool) -> dict[str, str]:
"""View pull request details."""
fields = "title,body,state,author,headRefName,baseRefName,comments,reviews"

result = run(
["gh", "pr", "view", str(pr_number), "--json", fields],
verbose,
)

if result.is_success():
import json

return json.loads(result.stdout)
return {}


def comment_on_pr(pr_number: int, comment: str, verbose: bool) -> bool:
"""Add a comment to a pull request."""
result = run(
["gh", "pr", "comment", str(pr_number), "--body", comment],
verbose,
)
return result.is_success()


def list_prs(state: str, verbose: bool) -> list[dict[str, str]]:
"""
List pull requests.
PR state filter ('open', 'closed', 'merged', 'all')
"""
result = run(
[
"gh",
"pr",
"list",
"--state",
state,
"--json",
"number,title,state,author,headRefName,baseRefName",
],
verbose,
)

if result.is_success():
import json

return json.loads(result.stdout)
return []


def merge_pr(
pr_number: int, merge_method: str, verbose: bool, delete_branch: bool = True
) -> bool:
"""
Merge a pull request.
Merge method ('merge', 'squash', 'rebase')
"""
command = ["gh", "pr", "merge", str(pr_number), f"--{merge_method}"]

if delete_branch:
command.append("--delete-branch")

result = run(command, verbose)
return result.is_success()


def close_pr(pr_number: int, verbose: bool, comment: Optional[str] = None) -> bool:
"""Close a pull request without merging."""
command = ["gh", "pr", "close", str(pr_number)]

if comment:
command.extend(["--comment", comment])

result = run(command, verbose)
return result.is_success()


def review_pr(pr_number: int, comment: str, action: str, verbose: bool) -> bool:
"""
Submit a review on a pull request.
Review action ('approve', 'request-changes', 'comment')
"""
command = [
"gh",
"pr",
"review",
str(pr_number),
"--body",
comment,
f"--{action}",
]

result = run(command, verbose)
return result.is_success()
91 changes: 91 additions & 0 deletions exercise_utils/roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import re
from typing import Optional

from exercise_utils import git, github_cli


class RoleMarker:
"""Wrapper for git and GitHub operations with automatic role marker formatting.

Usage:
bob = RoleMarker("teammate-bob")
bob.commit("Add feature", verbose=True)
# Creates: "[ROLE:teammate-bob] Add feature"
"""

PATTERN = re.compile(r"^\[ROLE:([a-zA-Z0-9_-]+)\]\s*", re.IGNORECASE)

def __init__(self, role: str) -> None:
"""Initialize RoleMarker with a specific role."""
self.role = role

@staticmethod
def format(role: str, text: str) -> str:
"""Format text with a role marker.
Example:
format('teammate-alice', 'Add feature') -> '[ROLE:teammate-alice] Add feature'
"""
return f"[ROLE:{role}] {text}"

@staticmethod
def extract_role(text: str) -> Optional[str]:
"""Extract role name from text with role marker if present."""
match = RoleMarker.PATTERN.match(text)
return match.group(1).lower() if match else None

@staticmethod
def has_role_marker(text: str) -> bool:
"""Check if text contains a role marker."""
return RoleMarker.PATTERN.match(text) is not None

@staticmethod
def strip_role_marker(text: str) -> str:
"""Remove role marker from text if present."""
return RoleMarker.PATTERN.sub("", text)

def _format_text(self, text: str) -> str:
"""Format text with this instance's role marker if not already present."""
if not self.has_role_marker(text):
return self.format(self.role, text)
return text

# Git operations with automatic role markers

def commit(self, message: str, verbose: bool) -> None:
"""Create a commit with automatic role marker."""
git.commit(self._format_text(message), verbose)

def merge_with_message(
self, target_branch: str, ff: bool, message: str, verbose: bool
) -> None:
"""Merge branches with custom message and automatic role marker."""
git.merge_with_message(target_branch, ff, self._format_text(message), verbose)

# GitHub PR operations with automatic role markers

def create_pr(
self, title: str, body: str, base: str, head: str, verbose: bool
) -> bool:
"""Create a pull request with automatic role markers."""
return github_cli.create_pr(
self._format_text(title), self._format_text(body), base, head, verbose
)

def comment_on_pr(self, pr_number: int, comment: str, verbose: bool) -> bool:
"""Add a comment to a pull request with automatic role marker."""
return github_cli.comment_on_pr(pr_number, self._format_text(comment), verbose)

def review_pr(
self, pr_number: int, comment: str, action: str, verbose: bool
) -> bool:
"""Submit a review on a pull request with automatic role marker. True if review was submitted successfully, False otherwise"""
return github_cli.review_pr(
pr_number, self._format_text(comment), action, verbose
)

def close_pr(
self, pr_number: int, verbose: bool, comment: Optional[str] = None
) -> bool:
"""Close a pull request without merging."""
formatted_comment = self._format_text(comment) if comment else None
return github_cli.close_pr(pr_number, verbose, formatted_comment)