Repository: All-Hands-AI/openhands-resolver

Issue:
Better handling of when a branch already exists
Currently, in `github_resolver/send_pull_request.py`, when pushing to github for a particular issue, the branch name is fixed here:
https://github.com/All-Hands-AI/openhands-resolver/blob/44aa2907d70852b7f98786c673304cc18b76d43e/openhands_resolver/send_pull_request.py#L162

However, if the github resolver has already tried to solve the issue and failed, the branch may already exist. Because of this, it would be better to check if the branch already exists, and if it does, further append `-try2`, `-try3`, until it finds a branch that doesn't exist.

Discussion:

Patch:
diff --git a/openhands_resolver/send_pull_request.py b/openhands_resolver/send_pull_request.py
index dd227b7..3d06d3e 100644
--- a/openhands_resolver/send_pull_request.py
+++ b/openhands_resolver/send_pull_request.py
@@ -139,6 +139,11 @@ def make_commit(repo_dir: str, issue: GithubIssue) -> None:
         raise RuntimeError("Failed to commit changes")
 
 
+
+def branch_exists(base_url: str, branch_name: str, headers: dict) -> bool:
+    response = requests.get(f"{base_url}/branches/{branch_name}", headers=headers)
+    return response.status_code == 200
+
 def send_pull_request(
     github_issue: GithubIssue,
     github_token: str,
@@ -158,15 +163,21 @@ def send_pull_request(
     }
     base_url = f"https://api.github.com/repos/{github_issue.owner}/{github_issue.repo}"
 
-    # Create a new branch
-    branch_name = f"openhands-fix-issue-{github_issue.number}"
+    # Create a new branch with a unique name
+    base_branch_name = f"openhands-fix-issue-{github_issue.number}"
+    branch_name = base_branch_name
+    attempt = 1
+
+    while branch_exists(base_url, branch_name, headers):
+        attempt += 1
+        branch_name = f"{base_branch_name}-try{attempt}"
 
     # Get the default branch
     response = requests.get(f"{base_url}", headers=headers)
     response.raise_for_status()
     default_branch = response.json()["default_branch"]
 
-    # Push changes to the new branch (using git command, as before)
+    # Create and checkout the new branch
     result = subprocess.run(
         f"git -C {patch_dir} checkout -b {branch_name}",
         shell=True,


Test patch:
diff --git a/tests/test_send_pull_request.py b/tests/test_send_pull_request.py
index b67edd5..fb2f393 100644
--- a/tests/test_send_pull_request.py
+++ b/tests/test_send_pull_request.py
@@ -213,7 +213,10 @@ def test_send_pull_request(
     repo_path = os.path.join(mock_output_dir, "repo")
 
     # Mock API responses
-    mock_get.return_value = MagicMock(json=lambda: {"default_branch": "main"})
+    mock_get.side_effect = [
+        MagicMock(status_code=404),  # Branch doesn't exist
+        MagicMock(json=lambda: {"default_branch": "main"})
+    ]
     mock_post.return_value.json.return_value = {
         "html_url": "https://github.com/test-owner/test-repo/pull/1"
     }
@@ -234,7 +237,7 @@ def test_send_pull_request(
     )
 
     # Assert API calls
-    assert mock_get.call_count == 1
+    assert mock_get.call_count == 2
 
     # Check branch creation and push
     assert mock_run.call_count == 2
@@ -521,6 +524,58 @@ def test_process_all_successful_issues(
     # Add more assertions as needed to verify the behavior of the function
 
 
+
+@patch('requests.get')
+@patch('subprocess.run')
+def test_send_pull_request_branch_naming(mock_run, mock_get, mock_github_issue, mock_output_dir):
+    repo_path = os.path.join(mock_output_dir, "repo")
+
+    # Mock API responses
+    mock_get.side_effect = [
+        MagicMock(status_code=200),  # First branch exists
+        MagicMock(status_code=200),  # Second branch exists
+        MagicMock(status_code=404),  # Third branch doesn't exist
+        MagicMock(json=lambda: {"default_branch": "main"}),  # Get default branch
+    ]
+
+    # Mock subprocess.run calls
+    mock_run.side_effect = [
+        MagicMock(returncode=0),  # git checkout -b
+        MagicMock(returncode=0),  # git push
+    ]
+
+    # Call the function
+    result = send_pull_request(
+        github_issue=mock_github_issue,
+        github_token="test-token",
+        github_username="test-user",
+        patch_dir=repo_path,
+        pr_type="branch",
+    )
+
+    # Assert API calls
+    assert mock_get.call_count == 4
+
+    # Check branch creation and push
+    assert mock_run.call_count == 2
+    checkout_call, push_call = mock_run.call_args_list
+
+    assert checkout_call == call(
+        f"git -C {repo_path} checkout -b openhands-fix-issue-42-try3",
+        shell=True,
+        capture_output=True,
+        text=True,
+    )
+    assert push_call == call(
+        f"git -C {repo_path} push https://test-user:test-token@github.com/test-owner/test-repo.git openhands-fix-issue-42-try3",
+        shell=True,
+        capture_output=True,
+        text=True,
+    )
+
+    # Check the result
+    assert result == "https://github.com/test-owner/test-repo/compare/openhands-fix-issue-42-try3?expand=1"
+
 @patch('openhands_resolver.send_pull_request.argparse.ArgumentParser')
 @patch('openhands_resolver.send_pull_request.process_all_successful_issues')
 @patch('openhands_resolver.send_pull_request.process_single_issue')
@@ -575,3 +630,4 @@ def test_main(mock_getenv, mock_path_exists, mock_load_single_resolver_output,
     mock_args.issue_number = 'invalid'
     with pytest.raises(ValueError):
         main()
+