From 25f238fa8c644d408262c03cd4ed371779343eb9 Mon Sep 17 00:00:00 2001 From: desmondwong1215 Date: Tue, 24 Feb 2026 01:33:23 +0800 Subject: [PATCH 1/3] Implement Role Marker --- exercise_utils/roles.py | 105 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 exercise_utils/roles.py diff --git a/exercise_utils/roles.py b/exercise_utils/roles.py new file mode 100644 index 00000000..bb446649 --- /dev/null +++ b/exercise_utils/roles.py @@ -0,0 +1,105 @@ +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 + ) -> str: + """Create a pull request with automatic role markers. + + Returns: + PR URL if successful, empty string otherwise + """ + 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. + + Returns: + True if comment was added successfully, False otherwise + """ + 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. + + Returns: + 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. + + Returns: + True if PR was closed successfully, False otherwise + """ + formatted_comment = self._format_text(comment) if comment else None + return github_cli.close_pr(pr_number, verbose, formatted_comment) From 5d03f952b20850436e58473da12d71506404f7a4 Mon Sep 17 00:00:00 2001 From: desmondwong1215 Date: Tue, 24 Feb 2026 04:18:12 +0800 Subject: [PATCH 2/3] Add wrapper for gh cli --- exercise_utils/github_cli.py | 118 +++++++++++++++++++++++++++++++++++ exercise_utils/roles.py | 50 +++++++-------- 2 files changed, 140 insertions(+), 28 deletions(-) diff --git a/exercise_utils/github_cli.py b/exercise_utils/github_cli.py index 1efcf279..8d6fa244 100644 --- a/exercise_utils/github_cli.py +++ b/exercise_utils/github_cli.py @@ -127,3 +127,121 @@ 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() diff --git a/exercise_utils/roles.py b/exercise_utils/roles.py index bb446649..46d0dbbf 100644 --- a/exercise_utils/roles.py +++ b/exercise_utils/roles.py @@ -19,6 +19,7 @@ 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. @@ -27,79 +28,72 @@ def format(role: str, text: str) -> str: """ 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. """ + """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 - ) -> str: - """Create a pull request with automatic role markers. + ) -> 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) - Returns: - PR URL if successful, empty string otherwise - """ - 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. - - Returns: - True if comment was added successfully, False otherwise - """ + """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. - - Returns: - 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. - - Returns: - True if PR was closed successfully, False otherwise + """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) From cebe0093e89db9e56246adf99f576a2d8979b37c Mon Sep 17 00:00:00 2001 From: desmondwong1215 Date: Tue, 24 Feb 2026 18:34:44 +0800 Subject: [PATCH 3/3] Reformat file --- exercise_utils/github_cli.py | 20 +++++++++----------- exercise_utils/roles.py | 24 ++++++++---------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/exercise_utils/github_cli.py b/exercise_utils/github_cli.py index 8d6fa244..6b049cb0 100644 --- a/exercise_utils/github_cli.py +++ b/exercise_utils/github_cli.py @@ -135,10 +135,14 @@ def create_pr(title: str, body: str, base: str, head: str, verbose: bool) -> boo "gh", "pr", "create", - "--title", title, - "--body", body, - "--base", base, - "--head", head, + "--title", + title, + "--body", + body, + "--base", + base, + "--head", + head, ] result = run(command, verbose) @@ -150,13 +154,7 @@ def view_pr(pr_number: int, verbose: bool) -> dict[str, str]: fields = "title,body,state,author,headRefName,baseRefName,comments,reviews" result = run( - [ - "gh", - "pr", - "view", - str(pr_number), - "--json", fields - ], + ["gh", "pr", "view", str(pr_number), "--json", fields], verbose, ) diff --git a/exercise_utils/roles.py b/exercise_utils/roles.py index 46d0dbbf..4f09b9aa 100644 --- a/exercise_utils/roles.py +++ b/exercise_utils/roles.py @@ -19,7 +19,6 @@ 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. @@ -28,68 +27,61 @@ def format(role: str, text: str) -> str: """ 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) - + """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) - + """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