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
36 changes: 31 additions & 5 deletions src/branch_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import git
from github.Issue import Issue
from typing import List, Optional, Tuple
from fuzzywuzzy import fuzz


def get_issue_related_branches(
Expand All @@ -13,13 +14,14 @@ def get_issue_related_branches(
) -> List[Tuple[str, bool]]:
"""
Uses `gh issue develop -l <issue_number>` to get all branches related to an issue number
and falls back to fuzzy matching if no branches are found through GitHub CLI

Args:
repo_path: Path to local git repository
issue_number: GitHub issue number to search for
issue: GitHub issue to search for

Returns:
List of tuples containing (branch_name, url)
List of tuples containing (branch_name, is_remote)
"""
issue_number = issue.number

Expand All @@ -39,13 +41,28 @@ def get_issue_related_branches(
print(f"Error getting related branches: {str(e)}")

if len(related_branches) == 0:

# Fall back to fuzzy matching if no branches found through GitHub CLI
repo = git.Repo(repo_path)

# Create a possible branch name based on issue number and title
possible_branch_name = f"{issue.number}-{'-'.join(issue.title.lower().split(' '))}"

# Also check for just the issue number in branch names
issue_number_str = str(issue.number)

# Set threshold for fuzzy matching (80% similarity)
fuzzy_threshold = 80

# Check local branches
for branch in repo.heads:
if possible_branch_name in branch.name:
# First check if branch contains the issue number
if issue_number_str in branch.name:
related_branches.append((branch.name, False))
continue

# Use partial_ratio for fuzzy matching to find similar branch names
similarity = fuzz.partial_ratio(possible_branch_name, branch.name)
if similarity > fuzzy_threshold:
related_branches.append((branch.name, False))

# Check remote branches
Expand All @@ -56,7 +73,16 @@ def get_issue_related_branches(
continue
# Remove remote name prefix for comparison
branch_name = ref.name.split('/', 1)[1]
if possible_branch_name in branch_name:

# First check if branch contains the issue number
if issue_number_str in branch_name:
related_branches.append((branch_name, True))
continue

# Use partial_ratio for fuzzy matching
similarity = fuzz.partial_ratio(
possible_branch_name, branch_name)
if similarity > fuzzy_threshold:
related_branches.append((branch_name, True))

os.chdir(orig_dir)
Expand Down
83 changes: 71 additions & 12 deletions src/git_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import subprocess
import git
from fuzzywuzzy import fuzz
from branch_handler import (
get_issue_related_branches,
get_current_branch,
Expand Down Expand Up @@ -167,6 +168,44 @@ def update_repository(repo_path: str) -> None:
origin.pull()


def select_best_branch(issue: Issue, branches: list) -> str:
"""
Select the best branch from multiple candidates using fuzzy matching

Args:
issue: The GitHub issue to match against
branches: List of branch names to choose from

Returns:
The best matching branch name
"""
issue_title = issue.title.lower()
issue_number = str(issue.number)

# Generate a normalized branch name from the issue
normalized_branch = f"{issue_number}-{'-'.join(issue_title.split())}"

# First try to find branches that contain the issue number
number_branches = [b for b in branches if issue_number in b]

if number_branches:
if len(number_branches) == 1:
# If only one branch with the issue number, return it
return number_branches[0]

# If multiple branches with issue number, use fuzzy matching on those
# Use token_sort_ratio for better matching with word order differences
best_branch = max(number_branches,
key=lambda b: fuzz.token_sort_ratio(normalized_branch, b))
return best_branch

# If no branches with issue number, use fuzzy matching on all branches
# Use token_sort_ratio for better matching with word order differences
best_branch = max(branches,
key=lambda b: fuzz.token_sort_ratio(normalized_branch, b))
return best_branch


def get_development_branch(issue: Issue, repo_path: str, create: bool = False) -> str:
"""
Gets or creates a development branch for an issue
Expand All @@ -187,8 +226,11 @@ def get_development_branch(issue: Issue, repo_path: str, create: bool = False) -
# Check for existing branches related to this issue
related_branches = get_issue_related_branches(repo_path, issue)

# Process branches with fuzzy matching scores
unique_branches = set([branch_name for branch_name, _ in related_branches])
branch_dict = {}

# Create a dictionary of branch names with their remote status
for branch_name in unique_branches:
branch_dict[branch_name] = []
wanted_inds = [i for i, (name, _) in enumerate(
Expand All @@ -199,15 +241,22 @@ def get_development_branch(issue: Issue, repo_path: str, create: bool = False) -
comments = get_issue_comments(issue)

if len(branch_dict) > 1:
branch_list = "\n".join(
[f"- {branch_name} : Remote = {is_remote}"
for branch_name, is_remote in branch_dict.items()]
)
error_msg = f"Found multiple branches for issue #{issue.number}:\n{branch_list}\n" +\
"Please delete or use existing branches before creating a new one."
if "Found multiple branches" not in comments[-1].body:
write_issue_response(issue, error_msg)
raise RuntimeError(error_msg)
# Try to select the best branch using fuzzy matching
try:
best_branch = select_best_branch(issue, list(branch_dict.keys()))
print(f"Selected best matching branch: {best_branch}")
return best_branch
except Exception as e:
# If selection fails, fall back to the error message
branch_list = "\n".join(
[f"- {branch_name} : Remote = {is_remote}"
for branch_name, is_remote in branch_dict.items()]
)
error_msg = f"Found multiple branches for issue #{issue.number}:\n{branch_list}\n" +\
"Please delete or use existing branches before creating a new one."
if "Found multiple branches" not in comments[-1].body:
write_issue_response(issue, error_msg)
raise RuntimeError(error_msg)
elif len(branch_dict) == 1:
return list(branch_dict.keys())[0]
elif create:
Expand Down Expand Up @@ -265,6 +314,9 @@ def create_pull_request(repo_path: str) -> str:
original_dir = os.getcwd()
os.chdir(repo_path)

# Get current branch name
current_branch = get_current_branch(repo_path)

# Create pull request
result = subprocess.run(['gh', 'pr', 'create', '--fill'],
check=True,
Expand All @@ -274,8 +326,9 @@ def create_pull_request(repo_path: str) -> str:
# Return to original directory
os.chdir(original_dir)

# Return the PR URL from the output
return result.stdout.strip()
# Return the PR URL from the output with branch information
pr_url = result.stdout.strip()
return pr_url, current_branch

except FileNotFoundError:
raise ValueError("GitHub CLI (gh) not found. Please install it first.")
Expand All @@ -295,7 +348,13 @@ def create_pull_request_from_issue(issue: Issue, repo_path: str) -> str:
URL of the created pull request
"""
branch = get_development_branch(issue, repo_path)
return create_pull_request(repo_path)
pr_url, branch_name = create_pull_request(repo_path)

# Add a comment to the issue with the branch name used for the PR
comment_text = f"Created pull request from branch: `{branch_name}`\n{pr_url}"
write_issue_response(issue, comment_text)

return pr_url


def push_changes_with_authentication(
Expand Down
1 change: 1 addition & 0 deletions src/response_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import json
import re
from urlextract import URLExtract
from fuzzywuzzy import fuzz

load_dotenv()

Expand Down