Repository: All-Hands-AI/openhands-resolver

Issue:
"Unterminated quoted string"
On a run of the github action, the following error was encountered:

```bash
Run if [ "true" == "true" ]; then
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/openhands_resolver/send_pull_request.py", line 388, in <module>
    main()
  File "/opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/openhands_resolver/send_pull_request.py", line 377, in main
    process_single_issue(
  File "/opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/openhands_resolver/send_pull_request.py", line 264, in process_single_issue
    make_commit(patched_repo_dir, resolver_output.issue)
  File "/opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/openhands_resolver/send_pull_request.py", line 139, in make_commit
    raise RuntimeError("Failed to commit changes")
RuntimeError: Failed to commit changes
Copied repository to output/patches/issue_20
Patch applied successfully
Git user configured as openhands
Error committing changes: /bin/sh: 1: Syntax error: Unterminated quoted string
```

The reason seems to be because in `openhands_resolver/send_pull_request.py`, the line here:
https://github.com/All-Hands-AI/openhands-resolver/blob/63dd2c9c905a375db53785c0a69e56951a279189/openhands_resolver/send_pull_request.py#L132

the `git commit` does not have `issue.title` properly escaped.

Once this is fixed, we should also bump the version to 0.1.6 and re-release.

Discussion:

Patch:
diff --git a/openhands_resolver/__init__.py b/openhands_resolver/__init__.py
index 1276d02..0a8da88 100644
--- a/openhands_resolver/__init__.py
+++ b/openhands_resolver/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "0.1.5"
+__version__ = "0.1.6"
diff --git a/openhands_resolver/send_pull_request.py b/openhands_resolver/send_pull_request.py
index 3d06d3e..ba24568 100644
--- a/openhands_resolver/send_pull_request.py
+++ b/openhands_resolver/send_pull_request.py
@@ -9,6 +9,7 @@ from openhands_resolver.io_utils import (
 from openhands_resolver.patching import parse_patch, apply_diff
 import requests
 import subprocess
+import shlex
 
 from openhands_resolver.resolver_output import ResolverOutput
 
@@ -128,8 +129,9 @@ def make_commit(repo_dir: str, issue: GithubIssue) -> None:
         print(f"Error adding files: {result.stderr}")
         raise RuntimeError("Failed to add files to git")
 
+    commit_message = f"Fix issue #{issue.number}: {shlex.quote(issue.title)}"
     result = subprocess.run(
-        f"git -C {repo_dir} commit -m 'Fix issue #{issue.number}: {issue.title}'",
+        f"git -C {repo_dir} commit -m {shlex.quote(commit_message)}",
         shell=True,
         capture_output=True,
         text=True,
@@ -251,7 +253,7 @@ def process_single_issue(
 ) -> None:
     if not resolver_output.success and not send_on_failure:
         print(
-            f"Issue {issue_number} was not successfully resolved. Skipping PR creation."
+            f"Issue {resolver_output.issue.number} was not successfully resolved. Skipping PR creation."
         )
         return
 
diff --git a/pyproject.toml b/pyproject.toml
index 2516498..005e349 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
 
 [tool.poetry]
 name = "openhands-resolver"
-version = "0.1.5"
+version = "0.1.6"
 description = "OpenHands Issue Resolver"
 authors = ["All Hands AI"]
 license = "MIT"


Test patch:
diff --git a/tests/test_send_pull_request.py b/tests/test_send_pull_request.py
index fb2f393..4853db3 100644
--- a/tests/test_send_pull_request.py
+++ b/tests/test_send_pull_request.py
@@ -1,6 +1,8 @@
 import os
+import shlex
 import tempfile
 import pytest
+import subprocess
 from unittest.mock import patch, MagicMock, call
 
 from openhands_resolver.send_pull_request import (
@@ -10,6 +12,7 @@ from openhands_resolver.send_pull_request import (
     process_single_issue,
     send_pull_request,
     process_all_successful_issues,
+    make_commit,
 )
 from openhands_resolver.resolver_output import ResolverOutput, GithubIssue
 
@@ -423,6 +426,52 @@ def test_process_single_issue(
     )
 
 
+@patch("openhands_resolver.send_pull_request.initialize_repo")
+@patch("openhands_resolver.send_pull_request.apply_patch")
+@patch("openhands_resolver.send_pull_request.send_pull_request")
+@patch("openhands_resolver.send_pull_request.make_commit")
+def test_process_single_issue_unsuccessful(
+    mock_make_commit,
+    mock_send_pull_request,
+    mock_apply_patch,
+    mock_initialize_repo,
+    mock_output_dir,
+):
+    # Initialize test data
+    github_token = "test_token"
+    github_username = "test_user"
+    pr_type = "draft"
+
+    resolver_output = ResolverOutput(
+        issue=GithubIssue(
+            owner="test-owner",
+            repo="test-repo",
+            number=1,
+            title="Issue 1",
+            body="Body 1",
+        ),
+        instruction="Test instruction 1",
+        base_commit="def456",
+        git_patch="Test patch 1",
+        history=[],
+        metrics={},
+        success=False,
+        success_explanation="",
+        error="Test error",
+    )
+
+    # Call the function
+    process_single_issue(
+        mock_output_dir, resolver_output, github_token, github_username, pr_type, None, False
+    )
+
+    # Assert that none of the mocked functions were called
+    mock_initialize_repo.assert_not_called()
+    mock_apply_patch.assert_not_called()
+    mock_make_commit.assert_not_called()
+    mock_send_pull_request.assert_not_called()
+
+
 @patch("openhands_resolver.send_pull_request.load_all_resolver_outputs")
 @patch("openhands_resolver.send_pull_request.process_single_issue")
 def test_process_all_successful_issues(
@@ -631,3 +680,30 @@ def test_main(mock_getenv, mock_path_exists, mock_load_single_resolver_output,
     with pytest.raises(ValueError):
         main()
 
+@patch('subprocess.run')
+def test_make_commit_escapes_issue_title(mock_subprocess_run):
+    # Setup
+    repo_dir = '/path/to/repo'
+    issue = GithubIssue(
+        owner='test-owner',
+        repo='test-repo',
+        number=42,
+        title='Issue with "quotes" and $pecial characters',
+        body='Test body'
+    )
+
+    # Mock subprocess.run to return success for all calls
+    mock_subprocess_run.return_value = MagicMock(returncode=0, stdout='', stderr='')
+
+    # Call the function
+    make_commit(repo_dir, issue)
+
+    # Assert that subprocess.run was called with the correct arguments
+    calls = mock_subprocess_run.call_args_list
+    assert len(calls) == 4  # git config check, git add, git commit
+
+    # Check the git commit call
+    git_commit_call = calls[3][0][0]
+    expected_commit_message = "Fix issue #42: 'Issue with \"quotes\" and $pecial characters'"
+    shlex_quote_message = shlex.quote(expected_commit_message)
+    assert f"git -C {repo_dir} commit -m {shlex_quote_message}" in git_commit_call