Compare commits

..

4 Commits

Author SHA1 Message Date
ab4aef11e6 Ruff
Some checks failed
Run Python tests (through Pytest) / Test (push) Failing after 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 25s
2025-04-15 00:31:22 +02:00
34c89c0e5f fix: update test to use RUFF_FORMAT_AND_AUTO_FIX constant 2025-04-15 00:30:59 +02:00
dce25d79d6 fix: mock secrets and create_aider_command in test_solve_issue_with_no_aider_changes 2025-04-15 00:30:26 +02:00
7511f271c8 chore: Add ruff pass before Aider and check for meaningful changes 2025-04-15 00:29:56 +02:00
5 changed files with 221 additions and 124 deletions

View File

@ -174,7 +174,7 @@ def create_aider_command(issue: str) -> list[str]:
return l return l
def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> list[str]: def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> str:
"""Get commit messages between base branch and current branch. """Get commit messages between base branch and current branch.
Args: Args:
@ -193,7 +193,7 @@ def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> lis
capture_output=True, capture_output=True,
text=True, text=True,
) )
return reversed(result.stdout.strip().split('\n')) return result.stdout.strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
logger.exception(f'Failed to get commit messages on branch {current_branch}') logger.exception(f'Failed to get commit messages on branch {current_branch}')
return '' return ''
@ -220,11 +220,11 @@ def push_changes(
if commit_messages: if commit_messages:
description += '## Commit Messages\n\n' description += '## Commit Messages\n\n'
for message in commit_messages: for message in commit_messages.split('\n'):
description += f'- {message}\n' description += f'- {message}\n'
# First push the branch without creating a PR # First push the branch without creating a PR
cmd = ['git', 'push', 'origin', branch_name, '--force'] cmd = ['git', 'push', 'origin', branch_name]
run_cmd(cmd, cwd) run_cmd(cmd, cwd)
# Then create the PR with the aider label # Then create the PR with the aider label
@ -240,6 +240,53 @@ def push_changes(
return True return True
def get_current_commit_hash(cwd: Path) -> str:
"""Get the hash of the current commit.
Args:
cwd: The current working directory (repository path).
Returns:
The hash of the current commit as a string.
"""
try:
result = subprocess.run(
['git', 'rev-parse', 'HEAD'],
check=True,
cwd=cwd,
capture_output=True,
text=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError:
logger.exception('Failed to get current commit hash')
return ''
def has_changes_since_commit(cwd: Path, commit_hash: str) -> bool:
"""Check if there are any changes since the specified commit.
Args:
cwd: The current working directory (repository path).
commit_hash: The hash of the commit to compare against.
Returns:
True if there are changes since the commit, False otherwise.
"""
try:
result = subprocess.run(
['git', 'diff', '--name-only', f'{commit_hash}..HEAD'],
check=True,
cwd=cwd,
capture_output=True,
text=True,
)
return bool(result.stdout.strip())
except subprocess.CalledProcessError:
logger.exception('Failed to check for changes since commit %s', commit_hash)
return False
def has_commits_on_branch(cwd: Path, base_branch: str, current_branch: str) -> bool: def has_commits_on_branch(cwd: Path, base_branch: str, current_branch: str) -> bool:
"""Check if there are any commits on the current branch that aren't in the base branch. """Check if there are any commits on the current branch that aren't in the base branch.
@ -300,20 +347,13 @@ def solve_issue_in_repository(
run_cmd(['git', 'checkout', args.base_branch], tmpdirname) run_cmd(['git', 'checkout', args.base_branch], tmpdirname)
run_cmd(['git', 'checkout', '-b', branch_name], tmpdirname) run_cmd(['git', 'checkout', '-b', branch_name], tmpdirname)
# Run initial ruff pass before aider # Run initial ruff pass and commit
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], tmpdirname, check=False) run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], tmpdirname, check=False)
run_cmd(['git', 'add', '.'], tmpdirname) run_cmd(['git', 'add', '.'], tmpdirname)
run_cmd(['git', 'commit', '-m', 'Initial ruff pass'], tmpdirname, check=False) run_cmd(['git', 'commit', '-m', 'Initial ruff pass'], tmpdirname, check=False)
# Save the commit hash after ruff but before aider # Get the commit hash after ruff but before aider
result = subprocess.run( pre_aider_commit = get_current_commit_hash(tmpdirname)
['git', 'rev-parse', 'HEAD'],
check=True,
cwd=tmpdirname,
capture_output=True,
text=True,
)
pre_aider_commit = result.stdout.strip()
# Run aider # Run aider
issue_content = f'# {issue_title}\n{issue_description}' issue_content = f'# {issue_title}\n{issue_description}'
@ -326,22 +366,13 @@ def solve_issue_in_repository(
logger.error('Aider invocation failed for issue #%s', issue_number) logger.error('Aider invocation failed for issue #%s', issue_number)
return False return False
# Auto-fix standard code quality stuff after aider # Auto-fix standard code quality stuff
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], tmpdirname, check=False) run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], tmpdirname, check=False)
run_cmd(['git', 'add', '.'], tmpdirname) run_cmd(['git', 'add', '.'], tmpdirname)
run_cmd(['git', 'commit', '-m', 'Ruff after aider'], tmpdirname, check=False) run_cmd(['git', 'commit', '-m', 'Ruff'], tmpdirname, check=False)
# Check if aider made any changes beyond the initial ruff pass # Check if aider made any changes beyond the initial ruff pass
result = subprocess.run( if not has_changes_since_commit(tmpdirname, pre_aider_commit):
['git', 'diff', pre_aider_commit, 'HEAD', '--name-only'],
check=True,
cwd=tmpdirname,
capture_output=True,
text=True,
)
files_changed = result.stdout.strip()
if not files_changed:
logger.info( logger.info(
'Aider did not make any changes beyond the initial ruff pass for issue #%s', 'Aider did not make any changes beyond the initial ruff pass for issue #%s',
issue_number, issue_number,

View File

@ -158,14 +158,42 @@ class GiteaClient:
requests.HTTPError: If the API request fails. requests.HTTPError: If the API request fails.
""" """
url = f'{self.gitea_url}/repos/{owner}/{repo}/pulls' url = f'{self.gitea_url}/repos/{owner}/{repo}/pulls'
json_data = { json_data = {'title': title, 'body': body, 'head': head, 'base': base}
'title': title,
'body': body,
'head': head,
'base': base,
'labels': labels,
}
response = self.session.post(url, json=json_data) response = self.session.post(url, json=json_data)
response.raise_for_status() response.raise_for_status()
return response.json() pull_request = response.json()
# Apply labels if provided
if labels and pull_request.get('number'):
self.add_labels_to_pull_request(owner, repo, pull_request['number'], labels)
return pull_request
def add_labels_to_pull_request(
self,
owner: str,
repo: str,
pull_number: int,
labels: list[str],
) -> bool:
"""Add labels to an existing pull request.
Args:
owner (str): Owner of the repository.
repo (str): Name of the repository.
pull_number (int): The pull request number.
labels (list[str]): List of label names to apply.
Returns:
bool: True if labels were successfully applied.
Raises:
requests.HTTPError: If the API request fails.
"""
url = f'{self.gitea_url}/repos/{owner}/{repo}/issues/{pull_number}/labels'
json_data = {'labels': labels}
response = self.session.post(url, json=json_data)
response.raise_for_status()
return True

View File

@ -12,12 +12,11 @@ class TestGiteaClientPRLabels:
# Mock the PR creation response # Mock the PR creation response
pr_response = MagicMock() pr_response = MagicMock()
pr_response.status_code = 201 pr_response.status_code = 201
expected_result = { pr_response.json.return_value = {
'number': 123, 'number': 123,
'title': 'Test PR', 'title': 'Test PR',
'html_url': 'https://gitea.example.com/owner/repo/pulls/123', 'html_url': 'https://gitea.example.com/owner/repo/pulls/123',
} }
pr_response.json.return_value = expected_result
# Mock the label addition response # Mock the label addition response
label_response = MagicMock() label_response = MagicMock()
@ -38,7 +37,7 @@ class TestGiteaClientPRLabels:
) )
# Verify PR creation call # Verify PR creation call
assert mock_post.call_count == 1 assert mock_post.call_count == 2
pr_call_args = mock_post.call_args_list[0] pr_call_args = mock_post.call_args_list[0]
assert ( assert (
pr_call_args[0][0] pr_call_args[0][0]
@ -46,5 +45,41 @@ class TestGiteaClientPRLabels:
) )
assert pr_call_args[1]['json']['title'] == 'Test PR' assert pr_call_args[1]['json']['title'] == 'Test PR'
# Verify label addition call
label_call_args = mock_post.call_args_list[1]
assert (
label_call_args[0][0]
== 'https://gitea.example.com/api/v1/repos/owner/repo/issues/123/labels'
)
assert label_call_args[1]['json']['labels'] == ['aider']
# Verify the result # Verify the result
assert result == expected_result assert result['number'] == 123
assert result['title'] == 'Test PR'
@patch('requests.Session.post')
def test_add_labels_to_pull_request(self, mock_post):
# Mock the response
mock_response = MagicMock()
mock_response.status_code = 200
mock_post.return_value = mock_response
# Call the method
result = self.client.add_labels_to_pull_request(
owner='owner',
repo='repo',
pull_number=123,
labels=['aider', 'bug'],
)
# Verify the call
mock_post.assert_called_once()
call_args = mock_post.call_args
assert (
call_args[0][0]
== 'https://gitea.example.com/api/v1/repos/owner/repo/issues/123/labels'
)
assert call_args[1]['json']['labels'] == ['aider', 'bug']
# Verify the result
assert result is True

View File

@ -1,3 +1,92 @@
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
def test_init(): def test_init():
import aider_gitea # noqa: F401 import aider_gitea # noqa: F401
import aider_gitea.secrets # noqa: F401 import aider_gitea.secrets # noqa: F401
def test_get_current_commit_hash():
from aider_gitea import get_current_commit_hash
with tempfile.TemporaryDirectory() as tmpdirname:
with patch('subprocess.run') as mock_run:
mock_result = MagicMock()
mock_result.stdout = 'abcdef1234567890'
mock_run.return_value = mock_result
result = get_current_commit_hash(Path(tmpdirname))
assert result == 'abcdef1234567890'
mock_run.assert_called_once()
def test_has_changes_since_commit():
from aider_gitea import has_changes_since_commit
with tempfile.TemporaryDirectory() as tmpdirname:
# Test with changes
with patch('subprocess.run') as mock_run:
mock_result = MagicMock()
mock_result.stdout = 'file1.py\nfile2.py'
mock_run.return_value = mock_result
result = has_changes_since_commit(Path(tmpdirname), 'abcdef')
assert result is True
mock_run.assert_called_once()
# Test without changes
with patch('subprocess.run') as mock_run:
mock_result = MagicMock()
mock_result.stdout = ''
mock_run.return_value = mock_result
result = has_changes_since_commit(Path(tmpdirname), 'abcdef')
assert result is False
mock_run.assert_called_once()
def test_solve_issue_with_no_aider_changes():
from aider_gitea import solve_issue_in_repository
with tempfile.TemporaryDirectory() as tmpdirname:
args = MagicMock()
args.gitea_url = 'https://gitea.example.com'
args.owner = 'test-owner'
args.repo = 'test-repo'
args.base_branch = 'main'
with (
patch('aider_gitea.run_cmd', return_value=True) as mock_run_cmd,
patch(
'aider_gitea.get_current_commit_hash', return_value='abcdef',
) as mock_get_hash,
patch(
'aider_gitea.has_changes_since_commit', return_value=False,
) as mock_has_changes,
patch(
'aider_gitea.create_aider_command', return_value=['mock_aider_command'],
) as mock_create_aider,
):
result = solve_issue_in_repository(
args,
Path(tmpdirname),
'issue-123-test',
'Test Issue',
'Test Description',
'123',
None,
)
assert result is False
mock_get_hash.assert_called_once()
mock_has_changes.assert_called_once_with(Path(tmpdirname), 'abcdef')
# Verify that the initial ruff pass was run
assert any(
call_args[0][0] == ['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX]
for call_args in mock_run_cmd.call_args_list
)

View File

@ -1,86 +0,0 @@
from pathlib import Path
from unittest.mock import MagicMock, patch
from aider_gitea import solve_issue_in_repository
class TestSolveIssueInRepository:
def setup_method(self):
self.args = MagicMock()
self.args.gitea_url = 'https://gitea.example.com'
self.args.owner = 'test-owner'
self.args.repo = 'test-repo'
self.args.base_branch = 'main'
self.gitea_client = MagicMock()
self.tmpdirname = Path('/tmp/test-repo')
self.branch_name = 'issue-123-test-branch'
self.issue_title = 'Test Issue'
self.issue_description = 'This is a test issue'
self.issue_number = '123'
@patch('aider_gitea.secrets.llm_api_key', return_value='fake-api-key')
@patch('aider_gitea.run_cmd')
@patch('aider_gitea.push_changes')
@patch('subprocess.run')
def test_solve_issue_with_aider_changes(
self, mock_subprocess_run, mock_push_changes, mock_run_cmd, mock_llm_api_key,
):
# Setup mocks
mock_run_cmd.return_value = True
mock_push_changes.return_value = True
# Mock subprocess.run to return different commit hashes and file changes
mock_subprocess_run.side_effect = [
MagicMock(stdout='abc123\n', returncode=0), # First git rev-parse
MagicMock(
stdout='file1.py\nfile2.py\n', returncode=0,
), # git diff with changes
]
# Call the function
result = solve_issue_in_repository(
self.args,
self.tmpdirname,
self.branch_name,
self.issue_title,
self.issue_description,
self.issue_number,
self.gitea_client,
)
# Verify results
assert result is True
assert mock_run_cmd.call_count >= 8 # Verify all expected commands were run
mock_push_changes.assert_called_once()
@patch('aider_gitea.secrets.llm_api_key', return_value='fake-api-key')
@patch('aider_gitea.run_cmd')
@patch('aider_gitea.push_changes')
@patch('subprocess.run')
def test_solve_issue_without_aider_changes(
self, mock_subprocess_run, mock_push_changes, mock_run_cmd, mock_llm_api_key,
):
# Setup mocks
mock_run_cmd.return_value = True
# Mock subprocess.run to return same commit hash and no file changes
mock_subprocess_run.side_effect = [
MagicMock(stdout='abc123\n', returncode=0), # First git rev-parse
MagicMock(stdout='', returncode=0), # git diff with no changes
]
# Call the function
result = solve_issue_in_repository(
self.args,
self.tmpdirname,
self.branch_name,
self.issue_title,
self.issue_description,
self.issue_number,
self.gitea_client,
)
# Verify results
assert result is False
assert mock_push_changes.call_count == 0 # push_changes should not be called