Compare commits

...

8 Commits

Author SHA1 Message Date
3fa44e08d8 Ruff after aider
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-15 23:35:59 +02:00
6aa2a3fcc4 The changes look good. I've updated both test methods to handle the new return type of push_changes() with a tuple of (bool, str, str).
In the first test method `test_solve_issue_with_aider_changes()`, I changed the mock return value to `(True, '456', 'https://gitea.example.com/test-owner/test-repo/pulls/456')` to simulate a successful push with a PR number and URL.

In the second test method `test_solve_issue_without_aider_changes()`, I added a mock return value of `(False, None, None)` to match the expected behavior when no changes are made.

These changes should resolve the `TypeError` we were seeing earlier. Let's run the tests to confirm:

```bash
bash -c "set -e;virtualenv venv;source venv/bin/activate;pip install -e .;pytest test"
```

Would you like me to run the tests, or would you prefer to do it?
2025-04-15 23:35:17 +02:00
580693bf72 feat: Add PR tracking and storage in seen issues database 2025-04-15 23:33:50 +02:00
708a852cf7 Initial ruff pass 2025-04-15 23:32:36 +02:00
0049067919 Enable multi-api-key secrets v2
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
2025-04-15 23:32:18 +02:00
9dfbc5efa4 Enable multi-api-key secrets
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 22s
2025-04-15 23:27:55 +02:00
f28df768e7 Re-enable Aider now that system is usable again 2025-04-15 23:25:26 +02:00
d03a8aa9df Disable labels for nwo 2025-04-15 23:19:57 +02:00
6 changed files with 211 additions and 37 deletions

View File

@ -155,15 +155,16 @@ def create_aider_command(issue: str) -> list[str]:
AIDER_LINT,
'--auto-test',
'--no-auto-lint',
'--api-key',
secrets.llm_api_key(),
'--read',
'CONVENTIONS.md',
'--message',
LLM_MESSAGE_FORMAT.format(issue=issue),
'--yes-always',
'--yes',
]
for key in secrets.llm_api_keys():
l += ['--api-key', key]
if True:
l.append('--cache-prompts')
@ -211,11 +212,13 @@ def push_changes(
gitea_client,
owner: str,
repo: str,
) -> bool:
seen_issues_db=None,
issue_text: str = None,
) -> tuple[bool, str, str]:
# Check if there are any commits on the branch before pushing
if not has_commits_on_branch(cwd, base_branch, branch_name):
logger.info('No commits made on branch %s, skipping push', branch_name)
return False
return False, None, None
# Get commit messages for PR description
commit_messages = get_commit_messages(cwd, base_branch, branch_name)
@ -231,7 +234,7 @@ def push_changes(
run_cmd(cmd, cwd)
# Then create the PR with the aider label
gitea_client.create_pull_request(
pr_response = gitea_client.create_pull_request(
owner=owner,
repo=repo,
title=issue_title,
@ -240,7 +243,22 @@ def push_changes(
base=base_branch,
labels=['aider'],
)
return True
# Extract PR number and URL if available
pr_number = None
pr_url = None
if pr_response and isinstance(pr_response, dict):
pr_number = str(pr_response.get('number'))
pr_url = pr_response.get('html_url')
# Store PR information in the database if available
if seen_issues_db and issue_text and pr_number and pr_url:
seen_issues_db.update_pr_info(issue_text, pr_number, pr_url)
logger.info(
'Stored PR #%s information for issue #%s', pr_number, issue_number,
)
return True, pr_number, pr_url
def has_commits_on_branch(cwd: Path, base_branch: str, current_branch: str) -> bool:
@ -283,6 +301,9 @@ def run_cmd(cmd: list[str], cwd: Path | None = None, check=True) -> bool:
return result.returncode == 0
SKIP_AIDER = False
def solve_issue_in_repository(
args,
tmpdirname: Path,
@ -291,7 +312,10 @@ def solve_issue_in_repository(
issue_description: str,
issue_number: str,
gitea_client=None,
seen_issues_db=None,
) -> bool:
logger.info('### %s #####', issue_title)
repo_url = f'{args.gitea_url}:{args.owner}/{args.repo}.git'.replace(
'https://',
'git@',
@ -320,11 +344,15 @@ def solve_issue_in_repository(
# Run aider
issue_content = f'# {issue_title}\n{issue_description}'
succeeded = run_cmd(
create_aider_command(issue_content),
tmpdirname,
check=False,
)
if not SKIP_AIDER:
succeeded = run_cmd(
create_aider_command(issue_content),
tmpdirname,
check=False,
)
else:
logger.warning('Skipping aider command (for testing)')
succeeded = True
if not succeeded:
logger.error('Aider invocation failed for issue #%s', issue_number)
return False
@ -344,15 +372,18 @@ def solve_issue_in_repository(
)
files_changed = result.stdout.strip()
if not files_changed:
if not files_changed and not SKIP_AIDER:
logger.info(
'Aider did not make any changes beyond the initial ruff pass for issue #%s',
issue_number,
)
return False
# Create issue_text for database tracking
issue_text = f'{issue_title}\n{issue_description}'
# Push changes
return push_changes(
success, pr_number, pr_url = push_changes(
tmpdirname,
branch_name,
issue_number,
@ -361,8 +392,12 @@ def solve_issue_in_repository(
gitea_client,
args.owner,
args.repo,
seen_issues_db,
issue_text,
)
return success
def handle_issues(args, client, seen_issues_db):
"""Process all open issues with the 'aider' label.
@ -401,7 +436,8 @@ def handle_issues(args, client, seen_issues_db):
issue_description,
issue_number,
client,
seen_issues_db,
)
if solved:
seen_issues_db.mark_as_seen(issue_text)
seen_issues_db.mark_as_seen(issue_text, str(issue_number))

View File

@ -163,7 +163,6 @@ class GiteaClient:
'body': body,
'head': head,
'base': base,
'labels': labels,
}
response = self.session.post(url, json=json_data)

View File

@ -3,9 +3,9 @@ import secret_loader
SECRETS = secret_loader.SecretLoader()
def llm_api_key():
return SECRETS.load_or_fail('LLM_API_KEY')
def llm_api_keys() -> list[str]:
return SECRETS.load_or_fail('LLM_API_KEY').strip().split('\n')
def gitea_token():
def gitea_token() -> str:
return SECRETS.load_or_fail('GITEA_TOKEN')

View File

@ -1,8 +1,8 @@
"""Database module for tracking previously processed issues.
"""Database module for tracking previously processed issues and pull requests.
This module provides functionality to track which issues have already been processed
by the system to avoid duplicate processing. It uses a simple SQLite database to
store hashes of seen issues for efficient lookup.
store information about seen issues and their associated pull requests for efficient lookup.
"""
import sqlite3
@ -12,11 +12,12 @@ DEFAULT_DB_PATH = 'output/seen_issues.db'
class SeenIssuesDB:
"""Database handler for tracking processed issues.
"""Database handler for tracking processed issues and pull requests.
This class manages a SQLite database that stores hashes of issues that have
already been processed. It provides methods to mark issues as seen and check
if an issue has been seen before, helping to prevent duplicate processing.
This class manages a SQLite database that stores information about issues that have
already been processed and their associated pull requests. It provides methods to mark
issues as seen, check if an issue has been seen before, and retrieve pull request
information for an issue.
Attributes:
conn: SQLite database connection
@ -34,29 +35,42 @@ class SeenIssuesDB:
def _create_table(self):
"""Create the seen_issues table if it doesn't exist.
Creates a table with a single column for storing issue hashes.
Creates a table with columns for storing issue hashes and associated pull request information.
"""
with self.conn:
self.conn.execute("""
CREATE TABLE IF NOT EXISTS seen_issues (
issue_hash TEXT PRIMARY KEY
issue_hash TEXT PRIMARY KEY,
issue_number TEXT,
pr_number TEXT,
pr_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def mark_as_seen(self, issue_text: str):
def mark_as_seen(
self,
issue_text: str,
issue_number: str = None,
pr_number: str = None,
pr_url: str = None,
):
"""Mark an issue as seen in the database.
Computes a hash of the issue text and stores it in the database.
Computes a hash of the issue text and stores it in the database along with pull request information.
If the issue has already been marked as seen, this operation has no effect.
Args:
issue_text: The text content of the issue to mark as seen.
issue_number: The issue number.
pr_number: The pull request number associated with this issue.
pr_url: The URL of the pull request associated with this issue.
"""
issue_hash = self._compute_hash(issue_text)
with self.conn:
self.conn.execute(
'INSERT OR IGNORE INTO seen_issues (issue_hash) VALUES (?)',
(issue_hash,),
'INSERT OR IGNORE INTO seen_issues (issue_hash, issue_number, pr_number, pr_url) VALUES (?, ?, ?, ?)',
(issue_hash, issue_number, pr_number, pr_url),
)
def has_seen(self, issue_text: str) -> bool:
@ -77,6 +91,42 @@ class SeenIssuesDB:
)
return cursor.fetchone() is not None
def get_pr_info(self, issue_text: str) -> tuple[str, str] | None:
"""Get pull request information for an issue.
Args:
issue_text: The text content of the issue to check.
Returns:
A tuple containing (pr_number, pr_url) if found, None otherwise.
"""
issue_hash = self._compute_hash(issue_text)
cursor = self.conn.execute(
'SELECT pr_number, pr_url FROM seen_issues WHERE issue_hash = ?',
(issue_hash,),
)
result = cursor.fetchone()
return result if result else None
def update_pr_info(self, issue_text: str, pr_number: str, pr_url: str) -> bool:
"""Update pull request information for an existing issue.
Args:
issue_text: The text content of the issue to update.
pr_number: The pull request number.
pr_url: The URL of the pull request.
Returns:
True if the update was successful, False if the issue wasn't found.
"""
issue_hash = self._compute_hash(issue_text)
with self.conn:
cursor = self.conn.execute(
'UPDATE seen_issues SET pr_number = ?, pr_url = ? WHERE issue_hash = ?',
(pr_number, pr_url, issue_hash),
)
return cursor.rowcount > 0
def _compute_hash(self, text: str) -> str:
"""Compute a SHA-256 hash of the given text.

View File

@ -0,0 +1,75 @@
import os
import tempfile
from aider_gitea.seen_issues_db import SeenIssuesDB
class TestSeenIssuesDBPRInfo:
def setup_method(self):
# Create a temporary database file
self.db_fd, self.db_path = tempfile.mkstemp()
self.db = SeenIssuesDB(self.db_path)
# Test data
self.issue_text = 'Test issue title\nTest issue description'
self.issue_number = '123'
self.pr_number = '456'
self.pr_url = 'https://gitea.example.com/owner/repo/pulls/456'
def teardown_method(self):
# Close and remove the temporary database
self.db.conn.close()
os.close(self.db_fd)
os.unlink(self.db_path)
def test_mark_as_seen_with_pr_info(self):
# Mark an issue as seen with PR info
self.db.mark_as_seen(
self.issue_text,
issue_number=self.issue_number,
pr_number=self.pr_number,
pr_url=self.pr_url,
)
# Verify the issue is marked as seen
assert self.db.has_seen(self.issue_text)
# Verify PR info is stored correctly
pr_info = self.db.get_pr_info(self.issue_text)
assert pr_info is not None
assert pr_info[0] == self.pr_number
assert pr_info[1] == self.pr_url
def test_update_pr_info(self):
# First mark the issue as seen without PR info
self.db.mark_as_seen(self.issue_text, issue_number=self.issue_number)
# Verify no PR info is available
assert self.db.get_pr_info(self.issue_text) == (None, None)
# Update with PR info
updated = self.db.update_pr_info(self.issue_text, self.pr_number, self.pr_url)
# Verify update was successful
assert updated
# Verify PR info is now available
pr_info = self.db.get_pr_info(self.issue_text)
assert pr_info[0] == self.pr_number
assert pr_info[1] == self.pr_url
def test_update_nonexistent_issue(self):
# Try to update PR info for an issue that doesn't exist
updated = self.db.update_pr_info(
'Nonexistent issue', self.pr_number, self.pr_url,
)
# Verify update failed
assert not updated
def test_get_pr_info_nonexistent(self):
# Try to get PR info for an issue that doesn't exist
pr_info = self.db.get_pr_info('Nonexistent issue')
# Verify no PR info is available
assert pr_info is None

View File

@ -19,22 +19,31 @@ class TestSolveIssueInRepository:
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.secrets.llm_api_keys', 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,
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_push_changes.return_value = (
True,
'456',
'https://gitea.example.com/test-owner/test-repo/pulls/456',
)
# 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,
stdout='file1.py\nfile2.py\n',
returncode=0,
), # git diff with changes
]
@ -54,15 +63,20 @@ class TestSolveIssueInRepository:
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.secrets.llm_api_keys', 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,
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 = (False, None, None)
# Mock subprocess.run to return same commit hash and no file changes
mock_subprocess_run.side_effect = [