Compare commits

...

67 Commits

Author SHA1 Message Date
14b236c1f8 🤖 Bumped version to 0.1.12
Some checks failed
Build Python Container / release-image (push) Failing after 18s
Package Python / Package-Python-And-Publish (push) Successful in 25s
Run Python tests (through Pytest) / Test (push) Failing after 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-07-07 01:01:05 +02:00
d12345d232 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-07-07 01:00:16 +02:00
6ae6cf31f2 🤖 Bumped version to 0.1.11
Some checks failed
Build Python Container / release-image (push) Failing after 18s
Package Python / Package-Python-And-Publish (push) Successful in 25s
Run Python tests (through Pytest) / Test (push) Failing after 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-07-07 00:48:21 +02:00
b5bf5d78ba 🤖 Repository layout updated to latest version
Some checks failed
Run Python tests (through Pytest) / Test (push) Failing after 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-06-13 23:59:24 +02:00
0497fd3e26 No default evaluator models
Some checks failed
Run Python tests (through Pytest) / Test (push) Failing after 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
2025-06-09 13:07:00 +02:00
ea9b55e4c3 Misc
Some checks failed
Run Python tests (through Pytest) / Test (push) Failing after 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
2025-06-09 13:00:44 +02:00
d0d22a8ac4 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-06-09 02:02:23 +02:00
5d28388bc3 Removed unneeded code 2025-06-09 01:55:36 +02:00
15fa6cef49 Update documentation for dual AI assistant support
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
Extended usage section in module docstring to cover both Aider and Claude Code integration:
- Clear explanation of automatic model routing based on model names
- Comprehensive command line examples for both assistants
- Updated Python API examples with new function signatures
- Environment configuration organized by assistant type
- Model examples categorized by routing destination

Users now have complete guidance on using either Aider or Claude Code with appropriate model selection.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-09 01:23:05 +02:00
325c0767f1 Add Claude Code integration with automatic model routing
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 26s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
Implemented complete Claude Code support alongside existing Aider integration:
- ClaudeCodeSolver strategy with programmatic Claude Code CLI usage
- Intelligent model detection to route Anthropic models to Claude Code
- Shared post-solver cleanup function to eliminate code duplication
- CLAUDE_CODE_MESSAGE_FORMAT constant for maintainable prompts
- Comprehensive test suite with 18 passing tests
- Automatic ANTHROPIC_API_KEY environment setup

Users can now specify any Anthropic model (claude, sonnet, haiku, opus) to use Claude Code, while other models continue using Aider.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-09 01:18:11 +02:00
0b9902c428 Refactor Aider integration using Strategy Pattern
Extracted Aider-specific functionality into AiderCodeSolver strategy class to enable future integration with Claude Code. This architectural change maintains backward compatibility while preparing for multi-AI-assistant support.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-09 01:02:45 +02:00
c476b1a37a 🤖 Bumped version to 0.1.10
Some checks failed
Build Python Container / release-image (push) Failing after 17s
Package Python / Package-Python-And-Publish (push) Successful in 25s
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
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-06-04 21:30:57 +02:00
43100e4708 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-06-04 21:30:19 +02:00
39a60fcc1b 🤖 Bumped version to 0.1.9
Some checks failed
Build Python Container / release-image (push) Failing after 18s
Package Python / Package-Python-And-Publish (push) Successful in 25s
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
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-27 00:35:06 +02:00
17b6dd8026 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-27 00:34:42 +02:00
12fbb3f5e0 🤖 Bumped version to 0.1.8
Some checks failed
Build Python Container / release-image (push) Failing after 18s
Package Python / Package-Python-And-Publish (push) Successful in 25s
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-21 12:29:29 +02:00
5308a5339b 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-21 12:29:13 +02:00
c83b0a4bc7 🤖 Bumped version to 0.1.7
Some checks failed
Build Python Container / Package-Container (push) Failing after 52s
Package Python / Package (push) Successful in 24s
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-21 00:57:18 +02:00
1a08677ea2 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-21 00:48:53 +02:00
7fcb9acb94 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-21 00:47:36 +02:00
42937ece1b Fix weird loop
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-05-13 23:55:40 +02:00
224e195726 More flexible yes/no
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-05-11 20:37:51 +02:00
3e5b88a736 Avoid emitting thinking tokens
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-05-11 16:37:51 +02:00
95b38b506e Update 2025-05-11 15:36:30 +02:00
7da687ab3f Config edit formats
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-05-11 11:55:46 +02:00
ef8903c3d7 unsloth use diff also 2025-05-11 11:28:06 +02:00
835235f41b Comment on status 2025-05-11 10:49:38 +02:00
3b99ebdea9 Silent ruff runs initially
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-05-11 10:45:14 +02:00
71c3a85e05 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-11 10:43:45 +02:00
56c70f6322 Ruff 2025-05-11 10:43:31 +02:00
32cdcde883 Messing around 2025-05-11 10:43:23 +02:00
4141eeb30c Always require model
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-05-10 19:53:47 +02:00
f72206365d 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-05-01 00:02:08 +02:00
bd5788ecae Merge branch 'main' into issue-93-handle-failing-pipelines
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-28 23:15:46 +02:00
a8ce6102d2 fix: return true early in verify_solution if no evaluator model is set
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-24 12:07:08 +02:00
236d1c0a10 Ruff after aider
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-24 11:55:25 +02:00
7a35029a18 feat: add CLI options to override aider and evaluator models 2025-04-24 11:55:21 +02:00
7a73a1e3fc Initial ruff pass 2025-04-24 11:54:41 +02:00
54cddfde0b Removed useless functionality
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-23 23:11:05 +02:00
b9013b7b2a Fixing types 2025-04-23 22:24:41 +02:00
6db1cccaf8 Fix the status_code 2025-04-23 22:20:02 +02:00
04b3baaba2 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 23s
2025-04-23 20:34:01 +02:00
cd9e32e4b0 fix: handle existing pull request error in create_pull_request to prevent crash 2025-04-23 20:33:57 +02:00
bff022b806 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 23s
2025-04-23 08:59:27 +02:00
c524891168 feat: add automatic handling and resolution of failing pipelines in PRs 2025-04-23 08:59:23 +02:00
e56463d207 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 23s
2025-04-21 16:19:04 +02:00
f253235841 feat: add handle_pr_comments to resolve and push PR review comments automatically 2025-04-21 16:19:00 +02:00
a4f7caf125 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 23s
2025-04-21 16:10:29 +02:00
7b500c3f2e feat: add handling of unresolved PR comments with context and auto-resolve via Aider 2025-04-21 16:09:04 +02:00
232622309f Ruff
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-21 14:41:51 +02:00
8b35ea9cad Fix 2025-04-21 14:41:39 +02:00
948ab5a382 refactor: push changes and create PR after every iteration in solve_issue_in_repository
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-21 12:50:05 +02:00
890dada71c Ruff after aider 2025-04-21 12:43:07 +02:00
3458826f54 refactor: push changes and create pull request after each iteration in solve_issue_in_repository 2025-04-21 12:43:03 +02:00
3e3e6591d7 Initial ruff pass 2025-04-21 12:42:33 +02:00
d10218f0c8 Remove useless test
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
2025-04-21 12:28:18 +02:00
e9a0719eb2 Stuff
Some checks failed
Run Python tests (through Pytest) / Test (push) Failing after 26s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-21 12:15:13 +02:00
f306faab16 Check the current code quality 2025-04-21 11:02:21 +02:00
727b788d01 Simplify 2025-04-21 10:21:24 +02:00
d196155bf7 Ruff after aider
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-16 00:08:56 +02:00
60a5900e33 refactor: use get_commit_messages in has_commits_on_branch 2025-04-16 00:08:56 +02:00
7083ca48c0 Resolving code debt
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-16 00:03:55 +02:00
78d2927b44 Store link in database
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 23s
2025-04-15 23:45:32 +02:00
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
17 changed files with 1313 additions and 349 deletions

View File

@ -1,3 +1,7 @@
# WARNING!
# THIS IS AN AUTOGENERATED FILE!
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
name: Build Python Container
on:
push:
@ -6,13 +10,72 @@ on:
paths-ignore: ['README.md', '.gitignore', 'LICENSE', 'CONVENTIONS.md', 'ruff.toml']
jobs:
Package-Container:
uses: jmaa/workflows/.gitea/workflows/container.yaml@v6.21
release-image:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- run: apt-get update
- name: Checkout
uses: actions/checkout@v3
- name: Setting up SSH
if: ${{ hashFiles('requirements_private.txt') != '' }}
uses: https://github.com/shimataro/ssh-key-action@v2.5.1
with:
REGISTRY_DOMAIN: gitfub.space
REGISTRY_ORGANIZATION: jmaa
secrets:
DOCKER_USERNAME: ${{ secrets.PIPY_REPO_USER }}
DOCKER_PASSWORD: ${{ secrets.PIPY_REPO_PASS }}
PIPELINE_WORKER_SSH_KEY: ${{ secrets.PIPELINE_WORKER_SSH_KEY }}
PIPELINE_WORKER_KNOWN_HOSTS: ${{ secrets.PIPELINE_WORKER_KNOWN_HOSTS }}
key: ${{ secrets.PIPELINE_WORKER_SSH_KEY }}
name: id_rsa
known_hosts: ${{ secrets.PIPELINE_WORKER_KNOWN_HOSTS }}
config: |
Host gitfub
HostName gitfub.space
User ${{ secrets.PIPY_REPO_USER }}
- name: Download private dependencies
if: ${{ hashFiles('requirements_private.txt') != '' }}
shell: bash
run: |
set -e
mkdir -p private_deps
cd private_deps
while IFS=$" " read -r -a dependency_spec
do
if test -n "${dependency_spec[1]}"
then
git clone -v --single-branch --no-tags "${dependency_spec[0]}" --branch "${dependency_spec[1]}"
else
git clone -v --single-branch --no-tags "${dependency_spec[0]}"
fi
done < ../requirements_private.txt
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
- name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: gitfub.space
username: ${{ secrets.PIPY_REPO_USER }}
password: ${{ secrets.PIPY_REPO_PASS }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
push: true
tags: |
gitfub.space/jmaa/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
gitfub.space/jmaa/${{ steps.meta.outputs.REPO_NAME }}:latest

View File

@ -1,3 +1,7 @@
# WARNING!
# THIS IS AN AUTOGENERATED FILE!
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
name: Package Python
on:
push:
@ -6,11 +10,24 @@ on:
paths-ignore: ['README.md', '.gitignore', 'LICENSE', 'CONVENTIONS.md', 'ruff.toml']
jobs:
Package:
uses: jmaa/workflows/.gitea/workflows/python-package.yaml@v6.21
with:
REGISTRY_DOMAIN: gitfub.space
REGISTRY_ORGANIZATION: jmaa
secrets:
PIPY_REPO_USER: ${{ secrets.PIPY_REPO_USER }}
PIPY_REPO_PASS: ${{ secrets.PIPY_REPO_PASS }}
Package-Python-And-Publish:
runs-on: ubuntu-latest
container:
image: node:21-bookworm
steps:
- name: Setting up Python ${{ env.PYTHON_VERSION }} for ${{runner.arch}} ${{runner.os}}
run: |
apt-get update
apt-get install -y python3 python3-pip
- name: Check out repository code
if: success()
uses: actions/checkout@v3
- name: Installing Python Dependencies
if: success()
run: python3 -m pip install --upgrade pip setuptools wheel build twine pytest --break-system-packages
- name: Build
if: success()
run: python3 -m build
- name: Publish
if: success()
run: python3 -m twine upload --repository-url "https://gitfub.space/api/packages/jmaa/pypi" -u ${{ secrets.PIPY_REPO_USER }} -p ${{ secrets.PIPY_REPO_PASS }} dist/*

View File

@ -1,3 +1,7 @@
# WARNING!
# THIS IS AN AUTOGENERATED FILE!
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
name: Run Python tests (through Pytest)
on:

View File

@ -1,3 +1,7 @@
# WARNING!
# THIS IS AN AUTOGENERATED FILE!
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
name: Verify Python project can be installed, loaded and have version checked
on:

View File

@ -1,3 +1,7 @@
<!-- WARNING! -->
<!-- THIS IS AN AUTOGENERATED FILE! -->
<!-- MANUAL CHANGES CAN AND WILL BE OVERWRITTEN! -->
# Conventions
When contributing code to this project, you MUST follow the requirements

101
README.md
View File

@ -1,23 +1,29 @@
<!--- WARNING --->
<!--- THIS IS AN AUTO-GENERATED FILE --->
<!--- MANUAL CHANGES CAN AND WILL BE OVERWRITTEN --->
<!-- WARNING! -->
<!-- THIS IS AN AUTOGENERATED FILE! -->
<!-- MANUAL CHANGES CAN AND WILL BE OVERWRITTEN! -->
# Aider Gitea
![Test program/library](https://gitfub.space/Jmaa/aider-gitea/actions/workflows/python-test.yml/badge.svg)
A code automation tool that integrates Gitea with Aider to automatically solve issues.
A code automation tool that integrates Gitea with AI assistants to automatically solve issues.
This program monitors your [Gitea](https://about.gitea.com/) repository for issues with the 'aider' label.
When such an issue is found, it:
1. Creates a new branch.
2. Invokes [Aider](https://aider.chat/) to solve the issue using a Large-Language Model.
2. Invokes an AI assistant (Aider or Claude Code) to solve the issue using a Large-Language Model.
3. Runs tests and code quality checks.
4. Creates a pull request with the solution.
The tool automatically selects the appropriate AI assistant based on the specified model:
- **Aider**: Used for non-Anthropic models (e.g., GPT, Ollama, Gemini)
- **Claude Code**: Used for Anthropic models (e.g., Claude, Sonnet, Haiku, Opus)
Inspired by [the AI workflows](https://github.com/oscoreio/ai-workflows/)
project.
## Usage
An application token must be supplied for the `gitea_token` secret. This must
@ -30,48 +36,109 @@ have the following permissions:
### Command Line
```bash
# Run with default settings
python -m aider_gitea
# Run with default settings (uses Aider)
python -m aider_gitea --aider-model gpt-4
# Use Claude Code with Anthropic models
python -m aider_gitea --aider-model claude-3-sonnet
python -m aider_gitea --aider-model claude-3-haiku
python -m aider_gitea --aider-model anthropic/claude-3-opus
# Use Aider with various models
python -m aider_gitea --aider-model gpt-4
python -m aider_gitea --aider-model ollama/llama3
python -m aider_gitea --aider-model gemini-pro
# Specify custom repository and owner
python -m aider_gitea --owner myorg --repo myproject
python -m aider_gitea --owner myorg --repo myproject --aider-model claude-3-sonnet
# Use a custom Gitea URL
python -m aider_gitea --gitea-url https://gitea.example.com
python -m aider_gitea --gitea-url https://gitea.example.com --aider-model gpt-4
# Specify a different base branch
python -m aider_gitea --base-branch develop
python -m aider_gitea --base-branch develop --aider-model claude-3-haiku
```
### AI Assistant Selection
The tool automatically routes to the appropriate AI assistant based on the model name:
**Claude Code Integration (Anthropic Models):**
- Model names containing: `claude`, `anthropic`, `sonnet`, `haiku`, `opus`
- Examples: `claude-3-sonnet`, `claude-3-haiku`, `anthropic/claude-3-opus`
- Requires: `ANTHROPIC_API_KEY` environment variable
**Aider Integration (All Other Models):**
- Any model not matching Anthropic patterns
- Examples: `gpt-4`, `ollama/llama3`, `gemini-pro`, `mistral-7b`
- Requires: `LLM_API_KEY` environment variable
### Python API
```python
from aider_gitea import solve_issue_in_repository
from aider_gitea import solve_issue_in_repository, create_code_solver
from pathlib import Path
import argparse
# Solve an issue programmatically
args = argparse.Namespace(
# Solve an issue programmatically with automatic AI assistant selection
repository_config = RepositoryConfig(
gitea_url="https://gitea.example.com",
owner="myorg",
repo="myproject",
base_branch="main"
)
# Set the model to control which AI assistant is used
import aider_gitea
aider_gitea.CODE_MODEL = "claude-3-sonnet" # Will use Claude Code
# aider_gitea.CODE_MODEL = "gpt-4" # Will use Aider
code_solver = create_code_solver() # Automatically selects based on model
solve_issue_in_repository(
args,
repository_config,
Path("/path/to/repo"),
"issue-123-fix-bug",
"Fix critical bug",
"The application crashes when processing large files",
"123"
"123",
gitea_client,
code_solver
)
```
### Environment Configuration
The tool uses environment variables for sensitive information:
**Required for all setups:**
- `GITEA_TOKEN`: Your Gitea API token
- `LLM_API_KEY`: API key for the language model used by Aider
**For Aider (non-Anthropic models):**
- `LLM_API_KEY`: API key for the language model (OpenAI, Ollama, etc.)
**For Claude Code (Anthropic models):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude models
### Model Examples
**Anthropic Models (→ Claude Code):**
```bash
--aider-model claude-3-sonnet
--aider-model claude-3-haiku
--aider-model claude-3-opus
--aider-model anthropic/claude-3-sonnet
```
**Non-Anthropic Models (→ Aider):**
```bash
--aider-model gpt-4
--aider-model gpt-3.5-turbo
--aider-model ollama/llama3
--aider-model ollama/codellama
--aider-model gemini-pro
--aider-model mistral-7b
```
```
## Dependencies

View File

@ -1,15 +1,19 @@
"""Aider Gitea.
A code automation tool that integrates Gitea with Aider to automatically solve issues.
A code automation tool that integrates Gitea with AI assistants to automatically solve issues.
This program monitors your [Gitea](https://about.gitea.com/) repository for issues with the 'aider' label.
When such an issue is found, it:
1. Creates a new branch.
2. Invokes [Aider](https://aider.chat/) to solve the issue using a Large-Language Model.
2. Invokes an AI assistant (Aider or Claude Code) to solve the issue using a Large-Language Model.
3. Runs tests and code quality checks.
4. Creates a pull request with the solution.
The tool automatically selects the appropriate AI assistant based on the specified model:
- **Aider**: Used for non-Anthropic models (e.g., GPT, Ollama, Gemini)
- **Claude Code**: Used for Anthropic models (e.g., Claude, Sonnet, Haiku, Opus)
Inspired by [the AI workflows](https://github.com/oscoreio/ai-workflows/)
project.
@ -25,51 +29,113 @@ have the following permissions:
### Command Line
```bash
# Run with default settings
python -m aider_gitea
# Run with default settings (uses Aider)
python -m aider_gitea --aider-model gpt-4
# Use Claude Code with Anthropic models
python -m aider_gitea --aider-model claude-3-sonnet
python -m aider_gitea --aider-model claude-3-haiku
python -m aider_gitea --aider-model anthropic/claude-3-opus
# Use Aider with various models
python -m aider_gitea --aider-model gpt-4
python -m aider_gitea --aider-model ollama/llama3
python -m aider_gitea --aider-model gemini-pro
# Specify custom repository and owner
python -m aider_gitea --owner myorg --repo myproject
python -m aider_gitea --owner myorg --repo myproject --aider-model claude-3-sonnet
# Use a custom Gitea URL
python -m aider_gitea --gitea-url https://gitea.example.com
python -m aider_gitea --gitea-url https://gitea.example.com --aider-model gpt-4
# Specify a different base branch
python -m aider_gitea --base-branch develop
python -m aider_gitea --base-branch develop --aider-model claude-3-haiku
```
### AI Assistant Selection
The tool automatically routes to the appropriate AI assistant based on the model name:
**Claude Code Integration (Anthropic Models):**
- Model names containing: `claude`, `anthropic`, `sonnet`, `haiku`, `opus`
- Examples: `claude-3-sonnet`, `claude-3-haiku`, `anthropic/claude-3-opus`
- Requires: `ANTHROPIC_API_KEY` environment variable
**Aider Integration (All Other Models):**
- Any model not matching Anthropic patterns
- Examples: `gpt-4`, `ollama/llama3`, `gemini-pro`, `mistral-7b`
- Requires: `LLM_API_KEY` environment variable
### Python API
```python
from aider_gitea import solve_issue_in_repository
from aider_gitea import solve_issue_in_repository, create_code_solver
from pathlib import Path
import argparse
# Solve an issue programmatically
args = argparse.Namespace(
# Solve an issue programmatically with automatic AI assistant selection
repository_config = RepositoryConfig(
gitea_url="https://gitea.example.com",
owner="myorg",
repo="myproject",
base_branch="main"
)
# Set the model to control which AI assistant is used
import aider_gitea
aider_gitea.CODE_MODEL = "claude-3-sonnet" # Will use Claude Code
# aider_gitea.CODE_MODEL = "gpt-4" # Will use Aider
code_solver = create_code_solver() # Automatically selects based on model
solve_issue_in_repository(
args,
repository_config,
Path("/path/to/repo"),
"issue-123-fix-bug",
"Fix critical bug",
"The application crashes when processing large files",
"123"
"123",
gitea_client,
code_solver
)
```
### Environment Configuration
The tool uses environment variables for sensitive information:
**Required for all setups:**
- `GITEA_TOKEN`: Your Gitea API token
- `LLM_API_KEY`: API key for the language model used by Aider
**For Aider (non-Anthropic models):**
- `LLM_API_KEY`: API key for the language model (OpenAI, Ollama, etc.)
**For Claude Code (Anthropic models):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude models
### Model Examples
**Anthropic Models ( Claude Code):**
```bash
--aider-model claude-3-sonnet
--aider-model claude-3-haiku
--aider-model claude-3-opus
--aider-model anthropic/claude-3-sonnet
```
**Non-Anthropic Models ( Aider):**
```bash
--aider-model gpt-4
--aider-model gpt-3.5-turbo
--aider-model ollama/llama3
--aider-model ollama/codellama
--aider-model gemini-pro
--aider-model mistral-7b
```
```
"""
import dataclasses
import logging
import re
import subprocess
@ -79,10 +145,36 @@ from pathlib import Path
from . import secrets
from ._version import __version__ # noqa: F401
from .seen_issues_db import SeenIssuesDB
logger = logging.getLogger(__name__)
@dataclasses.dataclass(frozen=True)
class RepositoryConfig:
gitea_url: str
owner: str
repo: str
base_branch: str
def repo_url(self) -> str:
return f'{self.gitea_url}:{self.owner}/{self.repo}.git'.replace(
'https://',
'git@',
)
@dataclasses.dataclass(frozen=True)
class IssueResolution:
success: bool
pull_request_url: str | None = None
pull_request_id: int | None = None
def __post_init__(self):
assert self.pull_request_id is None or isinstance(self.pull_request_id, int)
assert self.pull_request_url is None or isinstance(self.pull_request_url, str)
def generate_branch_name(issue_number: str, issue_title: str) -> str:
"""Create a branch name by sanitizing the issue title.
@ -107,17 +199,21 @@ def bash_cmd(*commands: str) -> str:
AIDER_TEST = bash_cmd(
'echo "Setting up virtual environment"',
'virtualenv venv',
'echo "Activating virtual environment"',
'source venv/bin/activate',
'echo "Installing package"',
'pip install -e .',
'echo "Testing package"',
'pytest test',
)
RUFF_FORMAT_AND_AUTO_FIX = bash_cmd(
'ruff format',
'ruff check --fix --ignore RUF022 --ignore PGH004',
'ruff format',
'ruff check --fix --ignore RUF022 --ignore PGH004',
'ruff format --silent',
'ruff check --fix --ignore RUF022 --ignore PGH004 --silent',
'ruff format --silent',
'ruff check --fix --ignore RUF022 --ignore PGH004 --silent',
)
AIDER_LINT = bash_cmd(
@ -127,22 +223,74 @@ AIDER_LINT = bash_cmd(
)
LLM_MESSAGE_FORMAT = """
{issue}
LLM_MESSAGE_FORMAT = """{issue}
# Solution Details
Go ahead with the changes you deem appropriate without waiting for explicit approval.
For code tasks:
1. Create a plan for how to solve the issue.
2. Write unit tests that proves that your solution works.
3. Then, solve the issue by writing the required code.
Do not draft changes beforehand; produce changes only once prompted for a specific file.
"""
MODEL = None
CLAUDE_CODE_MESSAGE_FORMAT = """{issue}
Please fix this issue by making the necessary code changes. Follow these guidelines:
1. Run tests after making changes to ensure they pass
2. Follow existing code style and conventions
3. Make minimal, focused changes to address the issue
4. Commit your changes with a descriptive message
The test command for this project is: {test_command}
The lint command for this project is: {lint_command}
"""
CODE_MODEL = None
EVALUATOR_MODEL = None
MODEL_EDIT_MODES = {
'ollama/qwen3:32b': 'diff',
'ollama/hf.co/unsloth/Qwen3-30B-A3B-GGUF:Q4_K_M': 'diff',
}
def create_aider_command(issue: str) -> list[str]:
def run_post_solver_cleanup(repository_path: Path, solver_name: str) -> None:
"""Run standard code quality fixes and commit changes after a code solver.
Args:
repository_path: Path to the repository
solver_name: Name of the solver (for commit message)
"""
# Auto-fix standard code quality stuff
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], repository_path, check=False)
run_cmd(['git', 'add', '.'], repository_path)
run_cmd(
['git', 'commit', '-m', f'Ruff after {solver_name}'],
repository_path,
check=False,
)
@dataclasses.dataclass(frozen=True)
class CodeSolverStrategy:
"""Base interface for code solving strategies."""
def solve_issue_round(self, repository_path: Path, issue_content: str) -> bool:
"""Attempt to solve an issue in a single round.
Args:
repository_path: Path to the repository
issue_content: The issue description to solve
Returns:
True if the solution round completed without crashing, False otherwise
"""
raise NotImplementedError
@dataclasses.dataclass(frozen=True)
class AiderCodeSolver(CodeSolverStrategy):
"""Code solver that uses Aider for issue resolution."""
def _create_aider_command(self, issue: str) -> list[str]:
"""Create the Aider command with all necessary flags."""
l = [
'aider',
'--chat-language',
@ -155,28 +303,133 @@ def create_aider_command(issue: str) -> list[str]:
AIDER_LINT,
'--auto-test',
'--no-auto-lint',
'--read',
'CONVENTIONS.md',
'--message',
LLM_MESSAGE_FORMAT.format(issue=issue),
'--yes',
'--disable-playwright',
'--timeout',
str(10_000),
]
if edit_format := MODEL_EDIT_MODES.get(CODE_MODEL):
l.append('--edit-format')
l.append(edit_format)
del edit_format
for key in secrets.llm_api_keys():
l += ['--api-key', key]
if False:
l.append('--read')
l.append('CONVENTIONS.md')
if True:
l.append('--cache-prompts')
if False:
l.append('--architect')
if MODEL:
if CODE_MODEL:
l.append('--model')
l.append(MODEL)
l.append(CODE_MODEL)
if CODE_MODEL.startswith('ollama/') and False:
l.append('--auto-lint')
if True:
l.append('--message')
l.append(LLM_MESSAGE_FORMAT.format(issue=issue))
return l
def solve_issue_round(self, repository_path: Path, issue_content: str) -> bool:
"""Solve an issue using Aider."""
# Primary Aider command
aider_command = self._create_aider_command(issue_content)
aider_did_not_crash = run_cmd(
aider_command,
cwd=repository_path,
check=False,
)
if not aider_did_not_crash:
return aider_did_not_crash
# Run post-solver cleanup
run_post_solver_cleanup(repository_path, 'aider')
return True
@dataclasses.dataclass(frozen=True)
class ClaudeCodeSolver(CodeSolverStrategy):
"""Code solver that uses Claude Code for issue resolution."""
def _create_claude_command(self, issue: str) -> list[str]:
"""Create the Claude Code command for programmatic use."""
cmd = [
'claude',
'-p',
'--output-format',
'stream-json',
#'--max-turns', '100',
'--debug',
'--verbose',
'--dangerously-skip-permissions',
]
if CODE_MODEL:
cmd.extend(['--model', CODE_MODEL])
cmd.append(issue)
return cmd
def solve_issue_round(self, repository_path: Path, issue_content: str) -> bool:
"""Solve an issue using Claude Code."""
# Prepare the issue prompt for Claude Code
enhanced_issue = CLAUDE_CODE_MESSAGE_FORMAT.format(
issue=issue_content,
test_command=AIDER_TEST,
lint_command=AIDER_LINT,
)
# Create Claude Code command
claude_command = self._create_claude_command(enhanced_issue)
# Run Claude Code
run_cmd(
claude_command,
cwd=repository_path,
check=False,
)
# Run post-solver cleanup
run_post_solver_cleanup(repository_path, 'Claude Code')
return True
def is_anthropic_model(model: str) -> bool:
"""Check if the model string indicates an Anthropic/Claude model."""
if not model:
return False
anthropic_indicators = [
'claude',
'anthropic',
'sonnet',
'haiku',
'opus',
]
model_lower = model.lower()
return any(indicator in model_lower for indicator in anthropic_indicators)
def create_code_solver() -> CodeSolverStrategy:
"""Create the appropriate code solver based on the configured model."""
if is_anthropic_model(CODE_MODEL):
return ClaudeCodeSolver()
else:
return AiderCodeSolver()
def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> list[str]:
"""Get commit messages between base branch and current branch.
@ -197,29 +450,42 @@ def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> lis
capture_output=True,
text=True,
)
return reversed(result.stdout.strip().split('\n'))
return list(reversed(result.stdout.strip().split('\n')))
except subprocess.CalledProcessError:
logger.exception(f'Failed to get commit messages on branch {current_branch}')
return ''
return []
def get_diff(cwd: Path, base_branch: str, current_branch: str) -> str:
result = subprocess.run(
['git', 'diff', f'{base_branch}..{current_branch}', '--pretty=format:%s'],
check=True,
cwd=cwd,
capture_output=True,
text=True,
)
return result.stdout.strip()
def push_changes(
repository_config: RepositoryConfig,
cwd: Path,
branch_name: str,
issue_number: str,
issue_title: str,
base_branch: str,
gitea_client,
owner: str,
repo: str,
) -> bool:
) -> IssueResolution:
# Check if there are any commits on the branch before pushing
if not has_commits_on_branch(cwd, base_branch, branch_name):
if not has_commits_on_branch(cwd, repository_config.base_branch, branch_name):
logger.info('No commits made on branch %s, skipping push', branch_name)
return False
return IssueResolution(False)
# Get commit messages for PR description
commit_messages = get_commit_messages(cwd, base_branch, branch_name)
commit_messages = get_commit_messages(
cwd,
repository_config.base_branch,
branch_name,
)
description = f'This pull request resolves #{issue_number}\n\n'
if commit_messages:
@ -232,16 +498,22 @@ def push_changes(
run_cmd(cmd, cwd)
# Then create the PR with the aider label
gitea_client.create_pull_request(
owner=owner,
repo=repo,
pr_response = gitea_client.create_pull_request(
owner=repository_config.owner,
repo=repository_config.repo,
title=issue_title,
body=description,
head=branch_name,
base=base_branch,
base=repository_config.base_branch,
labels=['aider'],
)
return True
# Extract PR number and URL if available
return IssueResolution(
True,
pr_response.get('html_url'),
int(pr_response.get('number')),
)
def has_commits_on_branch(cwd: Path, base_branch: str, current_branch: str) -> bool:
@ -256,15 +528,9 @@ def has_commits_on_branch(cwd: Path, base_branch: str, current_branch: str) -> b
True if there are commits on the current branch not in the base branch, False otherwise.
"""
try:
result = subprocess.run(
['git', 'log', f'{base_branch}..{current_branch}', '--oneline'],
check=True,
cwd=cwd,
capture_output=True,
text=True,
)
return bool(result.stdout.strip())
except subprocess.CalledProcessError:
commit_messages = get_commit_messages(cwd, base_branch, current_branch)
return bool(list(commit_messages))
except Exception:
logger.exception('Failed to check commits on branch %s', current_branch)
return False
@ -283,133 +549,318 @@ def run_cmd(cmd: list[str], cwd: Path | None = None, check=True) -> bool:
result = subprocess.run(cmd, check=check, cwd=cwd)
return result.returncode == 0
SKIP_AIDER = False
def remove_thinking_tokens(text: str) -> str:
text = re.sub(r'^\s*<think>.*?</think>', '', text, flags=re.MULTILINE | re.DOTALL)
text = text.strip()
return text
assert remove_thinking_tokens('<think>Hello</think>\nWorld\n') == 'World'
assert remove_thinking_tokens('<think>\nHello\n</think>\nWorld\n') == 'World'
assert remove_thinking_tokens('\n<think>\nHello\n</think>\nWorld\n') == 'World'
def run_ollama(cwd: Path, texts: list[str]) -> str:
cmd = ['ollama', 'run', EVALUATOR_MODEL.removeprefix('ollama/')]
process = subprocess.Popen(
cmd,
cwd=cwd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = process.communicate('\n'.join(texts))
stdout = remove_thinking_tokens(stdout)
return stdout
def parse_yes_no_answer(text: str) -> bool | None:
interword = '\n \t.,?-'
text = text.lower().strip(interword)
words = text.split(interword)
if words[-1] in {'yes', 'agree'}:
return True
if words[-1] in {'no', 'disagree'}:
return False
return None
assert parse_yes_no_answer('Yes.') == True
assert parse_yes_no_answer('no') == False
def run_ollama_and_get_yes_or_no(cwd, initial_texts: list[str]) -> bool:
texts = list(initial_texts)
texts.append('Think through your answer.')
while True:
response = run_ollama(cwd, texts)
yes_or_no = parse_yes_no_answer(response)
if yes_or_no is not None:
return yes_or_no
else:
texts.append(response)
texts.append('Please answer either "yes" or "no".')
def verify_solution(repository_path: Path, issue_content: str) -> bool:
if not EVALUATOR_MODEL:
return True
summary = run_ollama(
repository_path,
[
'Concisely summarize following changeset',
get_diff(repository_path, 'main', 'HEAD'),
],
)
return run_ollama_and_get_yes_or_no(
repository_path,
[
'Does this changeset accomplish the entire task?',
'# Change set',
summary,
'# Issue',
issue_content,
],
)
def get_head_commit_hash(repository_path: Path) -> str:
return subprocess.run(
['git', 'rev-parse', 'HEAD'],
check=True,
cwd=repository_path,
capture_output=True,
text=True,
).stdout.strip()
def solve_issue_in_repository(
args,
tmpdirname: Path,
repository_config: RepositoryConfig,
repository_path: Path,
branch_name: str,
issue_title: str,
issue_description: str,
issue_number: str,
gitea_client=None,
) -> bool:
logger.info("### %s #####", issue_title)
repo_url = f'{args.gitea_url}:{args.owner}/{args.repo}.git'.replace(
'https://',
'git@',
)
gitea_client,
code_solver: CodeSolverStrategy,
) -> IssueResolution:
logger.info('### %s #####', issue_title)
# Setup repository
run_cmd(['git', 'clone', repo_url, tmpdirname])
run_cmd(['bash', '-c', AIDER_TEST], tmpdirname)
run_cmd(['git', 'checkout', args.base_branch], tmpdirname)
run_cmd(['git', 'checkout', '-b', branch_name], tmpdirname)
run_cmd(['git', 'clone', repository_config.repo_url(), repository_path])
run_cmd(['bash', '-c', AIDER_TEST], repository_path)
run_cmd(['git', 'checkout', repository_config.base_branch], repository_path)
run_cmd(['git', 'checkout', '-b', branch_name], repository_path)
# Run initial ruff pass before aider
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], tmpdirname, check=False)
run_cmd(['git', 'add', '.'], tmpdirname)
run_cmd(['git', 'commit', '-m', 'Initial ruff pass'], tmpdirname, check=False)
# Run initial ruff pass before code solver
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], repository_path, check=False)
run_cmd(['git', 'add', '.'], repository_path)
run_cmd(['git', 'commit', '-m', 'Initial ruff pass'], repository_path, check=False)
# Save the commit hash after ruff but before aider
result = subprocess.run(
['git', 'rev-parse', 'HEAD'],
check=True,
cwd=tmpdirname,
capture_output=True,
text=True,
)
pre_aider_commit = result.stdout.strip()
# Run aider
# Run code solver
issue_content = f'# {issue_title}\n{issue_description}'
if not SKIP_AIDER:
succeeded = run_cmd(
create_aider_command(issue_content),
tmpdirname,
check=False,
while True:
# Save the commit hash after ruff but before code solver
pre_aider_commit = get_head_commit_hash(repository_path)
# Run code solver
solver_did_not_crash = code_solver.solve_issue_round(
repository_path,
issue_content,
)
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
if not solver_did_not_crash:
logger.error('Code solver invocation failed for issue #%s', issue_number)
return IssueResolution(False)
# Auto-fix standard code quality stuff after aider
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], tmpdirname, check=False)
run_cmd(['git', 'add', '.'], tmpdirname)
run_cmd(['git', 'commit', '-m', 'Ruff after aider'], tmpdirname, check=False)
# Check if aider made any changes beyond the initial ruff pass
result = subprocess.run(
['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 and not SKIP_AIDER:
logger.info(
'Aider did not make any changes beyond the initial ruff pass for issue #%s',
# Check if solver made any changes beyond the initial ruff pass
if not has_commits_on_branch(repository_path, pre_aider_commit, 'HEAD'):
logger.error(
'Code solver did not make any changes beyond the initial ruff pass for issue #%s',
issue_number,
)
return False
return IssueResolution(False)
# Push changes
return push_changes(
tmpdirname,
# Push changes and create/update the pull request on every iteration
resolution = push_changes(
repository_config,
repository_path,
branch_name,
issue_number,
issue_title,
args.base_branch,
gitea_client,
args.owner,
args.repo,
)
if not resolution.success:
return resolution
# Verify whether this is a satisfactory solution
if verify_solution(repository_path, issue_content):
return resolution
def handle_issues(args, client, seen_issues_db):
def solve_issues_in_repository(
repository_config: RepositoryConfig,
client,
seen_issues_db,
):
"""Process all open issues with the 'aider' label.
Args:
args: Command line arguments.
repository_config: Command line arguments.
client: The Gitea client instance.
seen_issues_db: Database of previously processed issues.
"""
try:
issues = client.get_issues(args.owner, args.repo)
issues = client.get_issues(repository_config.owner, repository_config.repo)
except Exception:
logger.exception('Failed to retrieve issues')
sys.exit(1)
if not issues:
logger.info('No issues found for %s', args.repo)
logger.info('No issues found for %s', repository_config.repo)
return
for issue in issues:
issue_url = issue.get('web_url')
issue_number = issue.get('number')
issue_description = issue.get('body', '')
title = issue.get('title', f'Issue {issue_number}')
issue_text = f'{title}\n{issue_description}'
if seen_issues_db.has_seen(issue_text):
if seen_issues_db.has_seen(issue_url):
logger.info('Skipping already processed issue #%s: %s', issue_number, title)
continue
else:
branch_name = generate_branch_name(issue_number, title)
with tempfile.TemporaryDirectory() as tmpdirname:
solved = solve_issue_in_repository(
args,
Path(tmpdirname),
code_solver = create_code_solver()
with tempfile.TemporaryDirectory() as repository_path:
issue_resolution = solve_issue_in_repository(
repository_config,
Path(repository_path),
branch_name,
title,
issue_description,
issue_number,
client,
code_solver,
)
seen_issues_db.mark_as_seen(issue_url, str(issue_number))
seen_issues_db.update_pr_info(
issue_url,
issue_resolution.pull_request_id,
issue_resolution.pull_request_url,
)
logger.info(
'Stored PR #%s information for issue #%s',
issue_resolution.pull_request_id,
issue_number,
)
if solved:
seen_issues_db.mark_as_seen(issue_text)
# TODO: PR comment handling disabled for now due to missing functionality
if False:
# Handle unresolved pull request comments
handle_pr_comments(
repository_config,
issue_resolution.pull_request_id,
branch_name,
Path(repository_path),
client,
seen_issues_db,
issue_url,
code_solver,
)
# Handle failing pipelines
handle_failing_pipelines(
repository_config,
issue_resolution.pull_request_id,
branch_name,
Path(repository_path),
client,
code_solver,
)
def handle_pr_comments(
repository_config,
pr_number: int,
branch_name,
repository_path,
client,
seen_issues_db,
issue_url,
code_solver: CodeSolverStrategy,
):
"""Fetch unresolved PR comments and resolve them via code solver."""
comments = client.get_pull_request_comments(
repository_config.owner,
repository_config.repo,
pr_number,
)
for comment in comments:
path = comment.get('path')
line = comment.get('line') or comment.get('position') or 0
file_path = repository_path / path
try:
lines = file_path.read_text().splitlines()
start = max(0, line - 3)
end = min(len(lines), line + 2)
context = '\n'.join(lines[start:end])
except Exception:
context = ''
body = comment.get('body', '')
issue = (
f'Resolve the following reviewer comment:\n{body}\n\n'
f'File: {path}\n\nContext:\n{context}'
)
# invoke code solver on the comment context
code_solver.solve_issue_round(repository_path, issue)
# commit and push changes for this comment
run_cmd(['git', 'add', path], repository_path, check=False)
run_cmd(
['git', 'commit', '-m', f'Resolve comment {comment.get("id")}'],
repository_path,
check=False,
)
run_cmd(['git', 'push', 'origin', branch_name], repository_path, check=False)
def handle_failing_pipelines(
repository_config: RepositoryConfig,
pr_number: str,
branch_name: str,
repository_path: Path,
client,
code_solver: CodeSolverStrategy,
) -> None:
"""Fetch failing pipelines for the given PR and resolve them via code solver."""
while True:
failed_runs = client.get_failed_pipelines(
repository_config.owner,
repository_config.repo,
pr_number,
)
if not failed_runs:
break
for run_id in failed_runs:
log = client.get_pipeline_log(
repository_config.owner,
repository_config.repo,
run_id,
)
lines = log.strip().split('\n')
context = '\n'.join(lines[-100:])
issue = f'Resolve the following failing pipeline run {run_id}:\n\n{context}'
code_solver.solve_issue_round(repository_path, issue)
run_cmd(['git', 'add', '.'], repository_path, check=False)
run_cmd(
['git', 'commit', '-m', f'Resolve pipeline {run_id}'],
repository_path,
check=False,
)
run_cmd(
['git', 'push', 'origin', branch_name],
repository_path,
check=False,
)

View File

@ -7,23 +7,14 @@ It assumes that the default branch (default "main") exists and that you have a v
import argparse
import logging
import time
from dataclasses import dataclass
from . import handle_issues, secrets
from . import RepositoryConfig, secrets, solve_issues_in_repository
from .gitea_client import GiteaClient
from .seen_issues_db import SeenIssuesDB
logger = logging.getLogger(__name__)
@dataclass
class AiderArgs:
gitea_url: str
owner: str
repo: str
base_branch: str
def parse_args():
parser = argparse.ArgumentParser(
description='Download issues and create pull requests for a Gitea repository.',
@ -51,9 +42,19 @@ def parse_args():
parser.add_argument(
'--interval',
type=int,
default=300,
default=30,
help='Interval in seconds between checks in daemon mode (default: 300)',
)
parser.add_argument(
'--aider-model',
help='Model to use for generating code (overrides default)',
required=True,
)
parser.add_argument(
'--evaluator-model',
help='Model to use for evaluating code (overrides default)',
default=None,
)
return parser.parse_args()
@ -61,6 +62,12 @@ def main():
logging.basicConfig(level='INFO')
args = parse_args()
# Override default models if provided
import aider_gitea as core
core.CODE_MODEL = args.aider_model
core.EVALUATOR_MODEL = args.evaluator_model
seen_issues_db = SeenIssuesDB()
client = GiteaClient(args.gitea_url, secrets.gitea_token())
@ -72,13 +79,13 @@ def main():
while True:
logger.info('Checking for new issues...')
for repo in repositories:
aider_args = AiderArgs(
repository_config = RepositoryConfig(
gitea_url=args.gitea_url,
owner=args.owner,
repo=repo,
base_branch=args.base_branch,
)
handle_issues(aider_args, client, seen_issues_db)
solve_issues_in_repository(repository_config, client, seen_issues_db)
del repo
if not args.daemon:
break

View File

@ -1 +1 @@
__version__ = '0.1.6'
__version__ = '0.1.12'

View File

@ -166,5 +166,56 @@ class GiteaClient:
}
response = self.session.post(url, json=json_data)
# If a pull request for this head/base already exists, return it instead of crashing
if response.status_code == 409:
logger.warning(
'Pull request already exists for head %s and base %s',
head,
base,
)
prs = self.get_pull_requests(owner, repo)
for pr in prs:
if (
pr.get('head', {}).get('ref') == head
and pr.get('base', {}).get('ref') == base
):
return pr
# fallback to raise if we cant find it
response.raise_for_status()
response.raise_for_status()
return response.json()
def get_failed_pipelines(self, owner: str, repo: str, pr_number: str) -> list[int]:
"""Fetch pipeline runs for a PR and return IDs of failed runs."""
url = f'{self.gitea_url}/repos/{owner}/{repo}/actions/runs'
response = self.session.get(url)
response.raise_for_status()
runs = response.json().get('workflow_runs', [])
failed = []
for run in runs:
if any(
pr.get('number') == int(pr_number)
for pr in run.get('pull_requests', [])
):
if run.get('conclusion') not in ('success',):
failed.append(run.get('id'))
return failed
def get_pipeline_log(self, owner: str, repo: str, run_id: int) -> str:
"""Download the logs for a pipeline run."""
url = f'{self.gitea_url}/repos/{owner}/{repo}/actions/runs/{run_id}/logs'
response = self.session.get(url)
response.raise_for_status()
return response.text
def get_pull_requests(
self,
owner: str,
repo: str,
state: str = 'open',
) -> list[dict]:
"""Fetch pull requests for a repository."""
url = f'{self.gitea_url}/repos/{owner}/{repo}/pulls?state={state}'
response = self.session.get(url)
response.raise_for_status()
return response.json()

View File

@ -9,3 +9,7 @@ def llm_api_keys() -> list[str]:
def gitea_token() -> str:
return SECRETS.load_or_fail('GITEA_TOKEN')
def anthropic_api_key() -> str:
return SECRETS.load_or_fail('ANTHROPIC_API_KEY')

View File

@ -1,22 +1,22 @@
"""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
from hashlib import sha256
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,56 +34,97 @@ 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_url TEXT PRIMARY KEY,
issue_number TEXT,
pr_number TEXT,
pr_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS resolved_comments (
issue_url TEXT,
comment_id TEXT,
PRIMARY KEY(issue_url, comment_id)
)
""")
def mark_as_seen(self, issue_text: str):
def mark_as_seen(
self,
issue_url: str,
issue_number: str | None = None,
pr_number: str | None = None,
pr_url: str | None = 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_url: 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_url, issue_number, pr_number, pr_url) VALUES (?, ?, ?, ?)',
(issue_url, issue_number, pr_number, pr_url),
)
def has_seen(self, issue_text: str) -> bool:
def has_seen(self, issue_url: str) -> bool:
"""Check if an issue has been seen before.
Computes a hash of the issue text and checks if it exists in the database.
Args:
issue_text: The text content of the issue to check.
issue_url: The text content of the issue to check.
Returns:
True if the issue has been seen before, False otherwise.
"""
issue_hash = self._compute_hash(issue_text)
cursor = self.conn.execute(
'SELECT 1 FROM seen_issues WHERE issue_hash = ?',
(issue_hash,),
'SELECT 1 FROM seen_issues WHERE issue_url = ?',
(issue_url,),
)
return cursor.fetchone() is not None
def _compute_hash(self, text: str) -> str:
"""Compute a SHA-256 hash of the given text.
def get_pr_info(self, issue_url: str) -> tuple[str, str] | None:
"""Get pull request information for an issue.
Args:
text: The text to hash.
issue_url: The text content of the issue to check.
Returns:
A hexadecimal string representation of the hash.
A tuple containing (pr_number, pr_url) if found, None otherwise.
"""
return sha256(text.encode('utf-8')).hexdigest()
cursor = self.conn.execute(
'SELECT pr_number, pr_url FROM seen_issues WHERE issue_url = ?',
(issue_url,),
)
result = cursor.fetchone()
return result if result else None
def update_pr_info(self, issue_url: str, pr_number: str, pr_url: str) -> bool:
"""Update pull request information for an existing issue.
Args:
issue_url: 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.
"""
with self.conn:
cursor = self.conn.execute(
'UPDATE seen_issues SET pr_number = ?, pr_url = ? WHERE issue_url = ?',
(pr_number, pr_url, issue_url),
)
return cursor.rowcount > 0

122
setup.py
View File

@ -1,10 +1,9 @@
# WARNING
#
# THIS IS AN AUTOGENERATED FILE.
#
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN.
# WARNING!
# THIS IS AN AUTOGENERATED FILE!
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
import re
from pathlib import Path
from setuptools import setup
@ -13,16 +12,23 @@ PACKAGE_NAME = 'aider_gitea'
PACKAGE_DESCRIPTION = """
Aider Gitea.
A code automation tool that integrates Gitea with Aider to automatically solve issues.
A code automation tool that integrates Gitea with AI assistants to automatically solve issues.
This program monitors your [Gitea](https://about.gitea.com/) repository for issues with the 'aider' label.
When such an issue is found, it:
1. Creates a new branch.
2. Invokes [Aider](https://aider.chat/) to solve the issue using a Large-Language Model.
2. Invokes an AI assistant (Aider or Claude Code) to solve the issue using a Large-Language Model.
3. Runs tests and code quality checks.
4. Creates a pull request with the solution.
The tool automatically selects the appropriate AI assistant based on the specified model:
- **Aider**: Used for non-Anthropic models (e.g., GPT, Ollama, Gemini)
- **Claude Code**: Used for Anthropic models (e.g., Claude, Sonnet, Haiku, Opus)
Inspired by [the AI workflows](https://github.com/oscoreio/ai-workflows/)
project.
## Usage
An application token must be supplied for the `gitea_token` secret. This must
@ -35,63 +41,137 @@ have the following permissions:
### Command Line
```bash
# Run with default settings
python -m aider_gitea
# Run with default settings (uses Aider)
python -m aider_gitea --aider-model gpt-4
# Use Claude Code with Anthropic models
python -m aider_gitea --aider-model claude-3-sonnet
python -m aider_gitea --aider-model claude-3-haiku
python -m aider_gitea --aider-model anthropic/claude-3-opus
# Use Aider with various models
python -m aider_gitea --aider-model gpt-4
python -m aider_gitea --aider-model ollama/llama3
python -m aider_gitea --aider-model gemini-pro
# Specify custom repository and owner
python -m aider_gitea --owner myorg --repo myproject
python -m aider_gitea --owner myorg --repo myproject --aider-model claude-3-sonnet
# Use a custom Gitea URL
python -m aider_gitea --gitea-url https://gitea.example.com
python -m aider_gitea --gitea-url https://gitea.example.com --aider-model gpt-4
# Specify a different base branch
python -m aider_gitea --base-branch develop
python -m aider_gitea --base-branch develop --aider-model claude-3-haiku
```
### AI Assistant Selection
The tool automatically routes to the appropriate AI assistant based on the model name:
**Claude Code Integration (Anthropic Models):**
- Model names containing: `claude`, `anthropic`, `sonnet`, `haiku`, `opus`
- Examples: `claude-3-sonnet`, `claude-3-haiku`, `anthropic/claude-3-opus`
- Requires: `ANTHROPIC_API_KEY` environment variable
**Aider Integration (All Other Models):**
- Any model not matching Anthropic patterns
- Examples: `gpt-4`, `ollama/llama3`, `gemini-pro`, `mistral-7b`
- Requires: `LLM_API_KEY` environment variable
### Python API
```python
from aider_gitea import solve_issue_in_repository
from aider_gitea import solve_issue_in_repository, create_code_solver
from pathlib import Path
import argparse
# Solve an issue programmatically
args = argparse.Namespace(
# Solve an issue programmatically with automatic AI assistant selection
repository_config = RepositoryConfig(
gitea_url="https://gitea.example.com",
owner="myorg",
repo="myproject",
base_branch="main"
)
# Set the model to control which AI assistant is used
import aider_gitea
aider_gitea.CODE_MODEL = "claude-3-sonnet" # Will use Claude Code
# aider_gitea.CODE_MODEL = "gpt-4" # Will use Aider
code_solver = create_code_solver() # Automatically selects based on model
solve_issue_in_repository(
args,
repository_config,
Path("/path/to/repo"),
"issue-123-fix-bug",
"Fix critical bug",
"The application crashes when processing large files",
"123"
"123",
gitea_client,
code_solver
)
```
### Environment Configuration
The tool uses environment variables for sensitive information:
**Required for all setups:**
- `GITEA_TOKEN`: Your Gitea API token
- `LLM_API_KEY`: API key for the language model used by Aider
**For Aider (non-Anthropic models):**
- `LLM_API_KEY`: API key for the language model (OpenAI, Ollama, etc.)
**For Claude Code (Anthropic models):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude models
### Model Examples
**Anthropic Models ( Claude Code):**
```bash
--aider-model claude-3-sonnet
--aider-model claude-3-haiku
--aider-model claude-3-opus
--aider-model anthropic/claude-3-sonnet
```
**Non-Anthropic Models ( Aider):**
```bash
--aider-model gpt-4
--aider-model gpt-3.5-turbo
--aider-model ollama/llama3
--aider-model ollama/codellama
--aider-model gemini-pro
--aider-model mistral-7b
```
```
""".strip()
PACKAGE_DESCRIPTION_SHORT = """
A code automation tool that integrates Gitea with Aider to automatically solve issues.""".strip()
A code automation tool that integrates Gitea with AI assistants to automatically solve issues.""".strip()
def parse_version_file(text: str) -> str:
match = re.match(r'^__version__\s*=\s*(["\'])([\d\.]+)\1$', text)
text = re.sub('^#.*', '', text, flags=re.MULTILINE)
match = re.match(r'^\s*__version__\s*=\s*(["\'])([\d\.]+)\1$', text)
if match is None:
msg = 'Malformed _version.py file!'
raise Exception(msg)
return match.group(2)
def find_python_packages() -> list[str]:
"""Find all python packages (directories containing __init__.py files)."""
root_path = Path(PACKAGE_NAME)
packages: set[str] = {PACKAGE_NAME}
# Search recursively
for init_file in root_path.rglob('__init__.py'):
packages.add(str(init_file.parent).replace('/', '.'))
return sorted(packages)
with open(PACKAGE_NAME + '/_version.py') as f:
version = parse_version_file(f.read())
@ -111,7 +191,7 @@ setup(
author='Jon Michael Aanes',
author_email='jonjmaa@gmail.com',
url='https://gitfub.space/Jmaa/' + PACKAGE_NAME,
packages=[PACKAGE_NAME],
packages=find_python_packages(),
install_requires=REQUIREMENTS_MAIN,
extras_require={
'test': REQUIREMENTS_TEST,

View File

@ -0,0 +1,122 @@
import pytest
from aider_gitea import (
AIDER_LINT,
AIDER_TEST,
CLAUDE_CODE_MESSAGE_FORMAT,
AiderCodeSolver,
ClaudeCodeSolver,
create_code_solver,
is_anthropic_model,
)
class TestClaudeCodeIntegration:
"""Test Claude Code integration and model routing logic."""
def test_is_anthropic_model_detection(self):
"""Test that Anthropic models are correctly detected."""
# Anthropic models should return True
assert is_anthropic_model('claude-3-sonnet')
assert is_anthropic_model('claude-3-haiku')
assert is_anthropic_model('claude-3-opus')
assert is_anthropic_model('anthropic/claude-3-sonnet')
assert is_anthropic_model('Claude-3-Sonnet') # Case insensitive
assert is_anthropic_model('ANTHROPIC/CLAUDE')
assert is_anthropic_model('some-sonnet-model')
assert is_anthropic_model('haiku-variant')
# Non-Anthropic models should return False
assert not is_anthropic_model('gpt-4')
assert not is_anthropic_model('gpt-3.5-turbo')
assert not is_anthropic_model('ollama/llama')
assert not is_anthropic_model('gemini-pro')
assert not is_anthropic_model('mistral-7b')
assert not is_anthropic_model('')
assert not is_anthropic_model(None)
def test_create_code_solver_routing(self, monkeypatch):
"""Test that the correct solver is created based on model."""
import aider_gitea
# Test Anthropic model routing
monkeypatch.setattr(aider_gitea, 'CODE_MODEL', 'claude-3-sonnet')
solver = create_code_solver()
assert isinstance(solver, ClaudeCodeSolver)
# Test non-Anthropic model routing
monkeypatch.setattr(aider_gitea, 'CODE_MODEL', 'gpt-4')
solver = create_code_solver()
assert isinstance(solver, AiderCodeSolver)
# Test None model routing (should default to Aider)
monkeypatch.setattr(aider_gitea, 'CODE_MODEL', None)
solver = create_code_solver()
assert isinstance(solver, AiderCodeSolver)
def test_claude_code_solver_command_creation(self):
"""Test that Claude Code commands are created correctly."""
import aider_gitea
solver = ClaudeCodeSolver()
issue = 'Fix the bug in the code'
# Test without model
with pytest.MonkeyPatch().context() as m:
m.setattr(aider_gitea, 'CODE_MODEL', None)
cmd = solver._create_claude_command(issue)
expected = [
'claude',
'-p',
'--output-format',
'json',
'--max-turns',
'10',
issue,
]
assert cmd == expected
# Test with model
with pytest.MonkeyPatch().context() as m:
m.setattr(aider_gitea, 'CODE_MODEL', 'claude-3-sonnet')
cmd = solver._create_claude_command(issue)
expected = [
'claude',
'-p',
'--output-format',
'json',
'--max-turns',
'10',
'--model',
'claude-3-sonnet',
issue,
]
assert cmd == expected
def test_claude_code_message_format(self):
"""Test that Claude Code message format works correctly."""
issue_content = 'Fix the authentication bug'
formatted_message = CLAUDE_CODE_MESSAGE_FORMAT.format(
issue=issue_content,
test_command=AIDER_TEST,
lint_command=AIDER_LINT,
)
# Verify the issue content is included
assert issue_content in formatted_message
# Verify the test and lint commands are included
assert AIDER_TEST in formatted_message
assert AIDER_LINT in formatted_message
# Verify the guidelines are present
assert 'Run tests after making changes' in formatted_message
assert 'Follow existing code style' in formatted_message
assert 'Make minimal, focused changes' in formatted_message
assert 'Commit your changes' in formatted_message
# Verify the structure contains placeholders that got replaced
assert '{issue}' not in formatted_message
assert '{test_command}' not in formatted_message
assert '{lint_command}' not in formatted_message

View File

@ -0,0 +1,58 @@
from pathlib import Path
from unittest.mock import patch
from aider_gitea import has_commits_on_branch
class TestHasCommitsOnBranch:
def setup_method(self):
self.cwd = Path('/tmp/test-repo')
self.base_branch = 'main'
self.current_branch = 'feature-branch'
@patch('aider_gitea.get_commit_messages')
def test_has_commits_true(self, mock_get_commit_messages):
# Setup mock to return some commit messages
mock_get_commit_messages.return_value = ['Commit 1', 'Commit 2']
# Test function returns True when there are commits
assert (
has_commits_on_branch(self.cwd, self.base_branch, self.current_branch)
is True
)
# Verify get_commit_messages was called with correct arguments
mock_get_commit_messages.assert_called_once_with(
self.cwd,
self.base_branch,
self.current_branch,
)
@patch('aider_gitea.get_commit_messages')
def test_has_commits_false(self, mock_get_commit_messages):
# Setup mock to return empty list
mock_get_commit_messages.return_value = []
# Test function returns False when there are no commits
assert (
has_commits_on_branch(self.cwd, self.base_branch, self.current_branch)
is False
)
# Verify get_commit_messages was called with correct arguments
mock_get_commit_messages.assert_called_once_with(
self.cwd,
self.base_branch,
self.current_branch,
)
@patch('aider_gitea.get_commit_messages')
def test_has_commits_exception(self, mock_get_commit_messages):
# Setup mock to raise an exception
mock_get_commit_messages.side_effect = Exception('Git command failed')
# Test function returns False when an exception occurs
assert (
has_commits_on_branch(self.cwd, self.base_branch, self.current_branch)
is False
)

View File

@ -0,0 +1,77 @@
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

@ -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_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,
):
# 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_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,
):
# 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