Compare commits

...

112 Commits

Author SHA1 Message Date
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
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
Jon Michael Aanes
d51070b535 Inpiration
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 10:19:05 +02:00
04619c9e5d 🤖 Bumped version to 0.1.6
Some checks failed
Build Python Container / Package-Container (push) Failing after 18s
Package Python / Package (push) Successful in 24s
Run Python tests (through Pytest) / Test (push) Successful in 27s
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-04-15 10:00:08 +02:00
e28add600d 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-15 09:58:59 +02:00
401942c578 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-15 09:56:24 +02:00
8d67641381 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-15 00:46:12 +02:00
6c4e17999b fix: mock secrets in tests to prevent LLM_API_KEY access 2025-04-15 00:43:32 +02:00
8f8b91bf5e refactor: Add ruff pass before aider and validate changes 2025-04-15 00:42:59 +02:00
5867cf299f Reverse commit order and force push
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-15 00:42:09 +02:00
d25dae34f0 Removed lots of redundant code
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-15 00:36:02 +02:00
903920bdfd Simplify
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 00:26:23 +02:00
07fca9b3c2 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-15 00:20:00 +02:00
2bb7a378ab feat: Add support for labeling pull requests as 'aider' during creation 2025-04-15 00:19:55 +02:00
5f6ee7f3ad Replace newlines with br
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-15 00:18:32 +02:00
92a0fc2715 Escape description 2025-04-15 00:08:48 +02:00
6585a6ed30 Document gitea api a little more
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-14 23:58:26 +02:00
f7bd033eae feat: add __init__.py to test directory to resolve namespace package issue
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
2025-04-14 21:57:56 +00:00
809ed44db1 refactor: replace f-strings with logging string formatting
Some checks failed
Verify Python project can be installed, loaded and have version checked / Test (push) Waiting to run
Run Python tests (through Pytest) / Test (push) Has been cancelled
2025-04-14 21:57:43 +00:00
3bdbf2e621 Ruff
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
2025-04-14 23:51:34 +02:00
7eab429f81 feat: include commit messages in pull request description 2025-04-14 23:51:30 +02:00
0c4fae510c Improved create_aider_command
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 23s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-14 23:48:42 +02:00
0109a40f8a Ruff
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
2025-04-14 21:48:01 +00:00
65d4a3028e refactor: Convert unittest assertions to pytest-style assert statements 2025-04-14 21:48:01 +00:00
3b4e40b2f1 Ruff
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
2025-04-14 21:47:24 +00:00
1593526d22 fix: Correct multi-line docstring summaries to start on first line 2025-04-14 21:47:24 +00:00
a40a11b1b9 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-14 23:42:40 +02:00
07126ad789 feat: add --no-analytics flag to aider command 2025-04-14 23:42:35 +02:00
d77991a543 Messing around with models
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 34s
2025-04-14 23:40:19 +02:00
5d2e003a39 Cache prompts
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-14 16:00:05 +02:00
7c5e4ead6c Fix logging
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-14 13:55:55 +02:00
4c5d2b08fd 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 23s
2025-04-13 23:40:10 +02:00
94ecf8d526 feat: introduce AiderArgs dataclass for better argument handling 2025-04-13 23:40:05 +02:00
ffd5fbb662 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-13 23:28:37 +02:00
976eec1a2e Fixed bug 2025-04-13 23:28:26 +02:00
820cc68b80 🤖 Bumped version to 0.1.5
All checks were successful
Package Python / Package (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
Run Python tests (through Pytest) / Test (push) Successful in 24s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 23:25:32 +02:00
26ebab48cb 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 23:25:08 +02:00
900bf39c3e Explicit logging
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-13 23:21:08 +02:00
273144b509 Iterate all user repos
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-13 23:19:36 +02:00
b4cbeb4691 Messing around with models
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-13 23:00:04 +02:00
ed5209f7dd feat: Support scanning all repos for a user in daemon mode
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
2025-04-13 21:55:32 +02:00
6b497bb225 🤖 Bumped version to 0.1.4
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
Package Python / Package (push) Successful in 25s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 18:48:16 +02:00
bba67d0b1a 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 18:47:56 +02:00
d1e7515e60 🤖 Bumped version to 0.1.3
Some checks are pending
Run Python tests (through Pytest) / Test (push) Waiting to run
Verify Python project can be installed, loaded and have version checked / Test (push) Waiting to run
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 18:47:51 +02:00
82dbe00f7f 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 18:47:51 +02:00
903973d7f5 docs: extend documentation with comprehensive usage examples and environment config
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-13 16:45:44 +00:00
fc85c982b9 Ruff
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
2025-04-13 18:42:40 +02:00
8d46519784 docs: Add comprehensive docstrings to seen_issues_db module 2025-04-13 18:42:36 +02:00
107ee6b55c 🤖 Bumped version to 0.1.2
All checks were successful
Package Python / Package (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-04-13 18:39:38 +02:00
3c9f04044a 🤖 Repository layout updated to latest version
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 18:39:16 +02:00
fd7d5f49e6 Updated root docs v2 2025-04-13 18:39:14 +02:00
15f6077b99 Updated root docs 2025-04-13 18:38:08 +02:00
1c4e13b7d0 Fixed tests
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-13 18:34:47 +02:00
c1786856c4 Ruff
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-04-13 18:31:52 +02:00
5fdf9cf002 Split into multiple files 2025-04-13 18:31:33 +02:00
77739b0004 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 23s
2025-04-13 18:27:10 +02:00
eeac3632b1 docs: Add comprehensive documentation to GiteaClient class 2025-04-13 18:27:05 +02:00
897aedf9f5 Update
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-13 18:24:41 +02:00
b4206ff41d Ruff check is allowed to fail
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-13 18:19:12 +02:00
230a4fd65d Do not run set -e for all cases
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-13 18:17:33 +02:00
08b07b5dda Run 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-13 18:14:35 +02:00
59e3efaf3c Code quality 2025-04-13 18:11:00 +02:00
8a77769500 Ruff 2025-04-13 18:06:56 +02:00
bdee056b67 Ruff 2025-04-13 18:06:44 +02:00
8c75a10b3a Autolint 2025-04-13 18:06:31 +02:00
f12e750194 feat: add commit check before pushing branch changes
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-13 18:02:05 +02:00
7953529cf2 feat: add daemon mode with configurable interval for continuous issue processing
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-13 16:00:28 +00:00
6c06637164 Autoselect model
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 25s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
2025-04-13 17:59:50 +02:00
da8ff0177a Only mark as seen after having processed
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-13 17:46:45 +02:00
877987787b Moved default path
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-13 17:44:28 +02:00
e463a8dbb3 Remove unneeded test code
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-13 17:29:12 +02:00
57fad60412 fix: update Python path in test to resolve ModuleNotFoundError
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-13 15:28:18 +00:00
d1af40658a fix: update Python path to include aider_gitea module for tests 2025-04-13 15:28:18 +00:00
d160344953 fix: add module path to Python path in test_seen_issues_db.py 2025-04-13 15:28:18 +00:00
06088a9fd4 feat: add SQLite handling for seen issues and update main processing logic 2025-04-13 15:28:18 +00:00
1f9252bcdb More explicit step-by-step
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-13 17:25:10 +02:00
343a646578 Force push doesnt work
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-13 17:21:08 +02:00
c4432e6f66 Force push to branch
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 22s
2025-04-13 17:14:23 +02:00
3f71159ecf Run within bash
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) Has been cancelled
2025-04-13 17:13:51 +02:00
024f987b8d Virtualenv 2025-04-13 17:06:13 +02:00
1d4cec29ed fixed generate_branch_name 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 22s
2025-04-13 16:54:24 +02:00
0d26bd7b06 fixed generate_branch_name
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-13 16:50:50 +02:00
298402ff2b feat: update branch name generation to sanitize issue title input
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 23s
2025-04-13 16:45:14 +02:00
4e014d4df4 feat: update branch name generation to include issue number and title 2025-04-13 16:45:09 +02:00
37c9ecf3db Place solution details after issue.
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 23s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-13 16:41:54 +02:00
51740bc312 Remember to push from the correct folder
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-13 16:39:36 +02:00
aa14a3e5e7 Removed old code for creating pull requests
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-13 16:37:16 +02:00
57f442284f Removed test for now
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-13 14:29:34 +00:00
b87e9a72ad test: Mock secret loading in test_push_changes to avoid failures 2025-04-13 14:29:34 +00:00
4896c6b6d3 test: mock secret loading in test_push_changes to avoid failures 2025-04-13 14:29:34 +00:00
4246a9a046 feat: add push_changes helper function and update process_issue logic 2025-04-13 14:29:34 +00:00
8c9194975b Fixed 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 22s
2025-04-13 16:23:53 +02:00
fca65f73c5 Lazy secrets
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-13 16:20:28 +02:00
d05e4c76f1 🤖 Bumped version to 0.1.1
All checks were successful
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 23s
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 16:15:12 +02:00
6c6c560a50 🤖 Repository layout updated to latest version
Some checks failed
Verify Python project can be installed, loaded and have version checked / Test (push) Waiting to run
Run Python tests (through Pytest) / Test (push) Has been cancelled
This commit was automatically generated by [a script](https://gitfub.space/Jmaa/repo-manager)
2025-04-13 16:14:46 +02:00
7e8cafcd09 Minimal documentation
Some checks failed
Verify Python project can be installed, loaded and have version checked / Test (push) Waiting to run
Run Python tests (through Pytest) / Test (push) Has been cancelled
2025-04-13 16:14:42 +02:00
d88cf1dd13 Added package requirements 2025-04-13 16:12:41 +02:00
8850ce06cb feat: update Aider command to instruct writing unit tests before solving issues
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 23s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-13 13:50:32 +00:00
7ef2e4cc38 Propogate version
All checks were successful
Run Python tests (through Pytest) / Test (push) Successful in 23s
Verify Python project can be installed, loaded and have version checked / Test (push) Successful in 22s
2025-04-13 15:47:23 +02:00
4be78a92a1 Merge pull request '[Issue 1] Aider should only work on issues that have been marked with the aider label.' (#4) from issue-1 into main
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 23s
Verify Python project can be installed, loaded and have version checked / Test (push) Failing after 21s
Reviewed-on: #4
2025-04-13 13:41:28 +00:00
3a147962ec Merge pull request '[Issue 2] Pull request branch name should contain text from the issue title' (#3) from issue-2 into main
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 24s
Verify Python project can be installed, loaded and have version checked / Test (push) Has been cancelled
Reviewed-on: #3
2025-04-13 13:40:56 +00:00
f5e7d5c379 feat: filter issues by the "aider" label in get_issues method
Some checks failed
Run Python tests (through Pytest) / Test (push) Successful in 23s
Verify Python project can be installed, loaded and have version checked / Test (push) Failing after 21s
2025-04-13 15:39:16 +02:00
21 changed files with 1337 additions and 147 deletions

View File

@ -0,0 +1,18 @@
name: Build Python Container
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
paths-ignore: ['README.md', '.gitignore', 'LICENSE', 'CONVENTIONS.md', 'ruff.toml']
jobs:
Package-Container:
uses: jmaa/workflows/.gitea/workflows/container.yaml@v6.21
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 }}

View File

@ -1,3 +1,10 @@
# Conventions
When contributing code to this project, you MUST follow the requirements
specified here.
## Code Conventions
When contributing code to this project, you MUST follow these principles:
- Code should be easy to read and understand.
@ -15,3 +22,11 @@ When contributing code to this project, you MUST follow these principles:
- Do not use f-strings in logging statements.
- Loop variables and walrus-expression-variables should be deleted when
unneeded to keep scope clean, and to avoid accidental use.
## Testing
When contributing test to this project, you MUST follow these principles:
- Do not use any testing libraries other than `pytest`.
- Mocking is the root of all evil. Writing your own stubs is much more
preferable.

20
LICENSE
View File

@ -1,3 +1,21 @@
MIT License
Copyright (c) 2025 Jon Michael Aanes
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -4,19 +4,89 @@
#
# 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.
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.
3. Runs tests and code quality checks.
4. Creates a pull request with the solution.
## Usage
An application token must be supplied for the `gitea_token` secret. This must
have the following permissions:
- `read:issue`: To be able to read issues on the specified repository.
- `write:repository`: To be able to create pull requests.
- `read:user`: Needed to iterate all user's repositories.
### Command Line
```bash
# Run with default settings
python -m aider_gitea
# Specify custom repository and owner
python -m aider_gitea --owner myorg --repo myproject
# Use a custom Gitea URL
python -m aider_gitea --gitea-url https://gitea.example.com
# Specify a different base branch
python -m aider_gitea --base-branch develop
```
### Python API
```python
from aider_gitea import solve_issue_in_repository
from pathlib import Path
# Solve an issue programmatically
args = argparse.Namespace(
gitea_url="https://gitea.example.com",
owner="myorg",
repo="myproject",
base_branch="main"
)
solve_issue_in_repository(
args,
Path("/path/to/repo"),
"issue-123-fix-bug",
"Fix critical bug",
"The application crashes when processing large files",
"123"
)
```
### Environment Configuration
The tool uses environment variables for sensitive information:
- `GITEA_TOKEN`: Your Gitea API token
- `LLM_API_KEY`: API key for the language model used by Aider
```
## Dependencies
This project requires [Python](https://www.python.org/) 3.8 or newer.
This project does not have any library requirements 😎
All required libraries can be installed easily using:
```bash
pip install -r requirements.txt
```
Full list of requirements:
- [secret_loader](https://gitfub.space/Jmaa/secret_loader)
## Contributing
@ -43,7 +113,25 @@ pytest --cov=aider_gitea test
## License
```
MIT License
Copyright (c) 2025 Jon Michael Aanes
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

View File

@ -1 +1,442 @@
# TODO
"""Aider Gitea.
A code automation tool that integrates Gitea with Aider 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.
3. Runs tests and code quality checks.
4. Creates a pull request with the solution.
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
have the following permissions:
- `read:issue`: To be able to read issues on the specified repository.
- `write:repository`: To be able to create pull requests.
- `read:user`: Needed to iterate all user's repositories.
### Command Line
```bash
# Run with default settings
python -m aider_gitea
# Specify custom repository and owner
python -m aider_gitea --owner myorg --repo myproject
# Use a custom Gitea URL
python -m aider_gitea --gitea-url https://gitea.example.com
# Specify a different base branch
python -m aider_gitea --base-branch develop
```
### Python API
```python
from aider_gitea import solve_issue_in_repository
from pathlib import Path
# Solve an issue programmatically
args = argparse.Namespace(
gitea_url="https://gitea.example.com",
owner="myorg",
repo="myproject",
base_branch="main"
)
solve_issue_in_repository(
args,
Path("/path/to/repo"),
"issue-123-fix-bug",
"Fix critical bug",
"The application crashes when processing large files",
"123"
)
```
### Environment Configuration
The tool uses environment variables for sensitive information:
- `GITEA_TOKEN`: Your Gitea API token
- `LLM_API_KEY`: API key for the language model used by Aider
```
"""
import dataclasses
import logging
import re
import subprocess
import sys
import tempfile
from pathlib import Path
from . import secrets
from ._version import __version__ # noqa: F401
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: str | None = None
def generate_branch_name(issue_number: str, issue_title: str) -> str:
"""Create a branch name by sanitizing the issue title.
Non-alphanumeric characters (except spaces) are removed,
the text is lowercased, and spaces are replaced with dashes.
Args:
issue_number: The issue number to include in the branch name.
issue_title: The issue title to sanitize and include in the branch name.
Returns:
A sanitized branch name combining the issue number and title.
"""
sanitized = re.sub(r'[^0-9a-zA-Z ]+', '', issue_title)
parts = ['issue', str(issue_number), *sanitized.lower().split()]
return '-'.join(parts)
def bash_cmd(*commands: str) -> str:
commands = ('set -e', *commands)
return 'bash -c "' + ';'.join(commands) + '"'
AIDER_TEST = bash_cmd(
'virtualenv venv',
'source venv/bin/activate',
'pip install -e .',
'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',
)
AIDER_LINT = bash_cmd(
RUFF_FORMAT_AND_AUTO_FIX,
'ruff format',
'ruff check --ignore RUF022 --ignore PGH004',
)
LLM_MESSAGE_FORMAT = """
{issue}
# Solution Details
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.
"""
MODEL = None
def create_aider_command(issue: str) -> list[str]:
l = [
'aider',
'--chat-language',
'english',
'--no-stream',
'--no-analytics',
'--test-cmd',
AIDER_TEST,
'--lint-cmd',
AIDER_LINT,
'--auto-test',
'--no-auto-lint',
'--read',
'CONVENTIONS.md',
'--message',
LLM_MESSAGE_FORMAT.format(issue=issue),
'--yes',
]
for key in secrets.llm_api_keys():
l += ['--api-key', key]
if True:
l.append('--cache-prompts')
if False:
l.append('--architect')
if MODEL:
l.append('--model')
l.append(MODEL)
return l
def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> list[str]:
"""Get commit messages between base branch and current branch.
Args:
cwd: The current working directory (repository path).
base_branch: The name of the base branch to compare against.
current_branch: The name of the current branch to check for commits.
Returns:
A string containing all commit messages, one per line.
"""
try:
result = subprocess.run(
['git', 'log', f'{base_branch}..{current_branch}', '--pretty=format:%s'],
check=True,
cwd=cwd,
capture_output=True,
text=True,
)
return list(reversed(result.stdout.strip().split('\n')))
except subprocess.CalledProcessError:
logger.exception(f'Failed to get commit messages on branch {current_branch}')
return []
def push_changes(
repository_config: RepositoryConfig,
cwd: Path,
branch_name: str,
issue_number: str,
issue_title: str,
gitea_client,
) -> IssueResolution:
# Check if there are any commits on the branch before pushing
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 IssueResolution(False)
# Get commit messages for PR description
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:
description += '## Commit Messages\n\n'
for message in commit_messages:
description += f'- {message}\n'
# First push the branch without creating a PR
cmd = ['git', 'push', 'origin', branch_name, '--force']
run_cmd(cmd, cwd)
# Then create the PR with the aider label
pr_response = gitea_client.create_pull_request(
owner=repository_config.owner,
repo=repository_config.repo,
title=issue_title,
body=description,
head=branch_name,
base=repository_config.base_branch,
labels=['aider'],
)
# Extract PR number and URL if available
return IssueResolution(
True, str(pr_response.get('number')), pr_response.get('html_url'),
)
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.
Args:
cwd: The current working directory (repository path).
base_branch: The name of the base branch to compare against.
current_branch: The name of the current branch to check for commits.
Returns:
True if there are commits on the current branch not in the base branch, False otherwise.
"""
try:
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
def run_cmd(cmd: list[str], cwd: Path | None = None, check=True) -> bool:
"""Run a shell command and return its success status.
Args:
cmd: The command to run as a list of strings.
cwd: The directory to run the command in.
check: Whether to raise an exception if the command fails.
Returns:
True if the command succeeded, False otherwise.
"""
result = subprocess.run(cmd, check=check, cwd=cwd)
return result.returncode == 0
SKIP_AIDER = False
def solve_issue_in_repository(
repository_config: RepositoryConfig,
tmpdirname: Path,
branch_name: str,
issue_title: str,
issue_description: str,
issue_number: str,
gitea_client,
) -> IssueResolution:
logger.info('### %s #####', issue_title)
# Setup repository
run_cmd(['git', 'clone', repository_config.repo_url(), tmpdirname])
run_cmd(['bash', '-c', AIDER_TEST], tmpdirname)
run_cmd(['git', 'checkout', repository_config.base_branch], tmpdirname)
run_cmd(['git', 'checkout', '-b', branch_name], tmpdirname)
# 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)
# 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
issue_content = f'# {issue_title}\n{issue_description}'
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 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',
issue_number,
)
return IssueResolution(False)
# Push changes
return push_changes(
repository_config,
tmpdirname,
branch_name,
issue_number,
issue_title,
gitea_client,
)
def solve_issues_in_repository(
repository_config: RepositoryConfig, client, seen_issues_db,
):
"""Process all open issues with the 'aider' label.
Args:
repository_config: Command line arguments.
client: The Gitea client instance.
seen_issues_db: Database of previously processed issues.
"""
try:
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', 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}')
if seen_issues_db.has_seen(issue_url):
logger.info('Skipping already processed issue #%s: %s', issue_number, title)
continue
branch_name = generate_branch_name(issue_number, title)
with tempfile.TemporaryDirectory() as tmpdirname:
issue_resolution = solve_issue_in_repository(
repository_config,
Path(tmpdirname),
branch_name,
title,
issue_description,
issue_number,
client,
)
if issue_resolution.success:
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,
)

View File

@ -4,158 +4,78 @@ This script downloads issues from a given Gitea repository and produces a pull r
It assumes that the default branch (default "main") exists and that you have a valid API token if authentication is required.
"""
import logging
from pathlib import Path
import argparse
import requests
import sys
import dataclasses
import tempfile
import subprocess
import os
import logging
import time
import secret_loader
import re
def generate_branch_name(issue_title: str) -> str:
"""
Create a branch name by sanitizing the issue title.
Non-alphanumeric characters (except spaces) are removed,
the text is lowercased, and spaces are replaced with dashes.
"""
sanitized = re.sub(r"[^0-9a-zA-Z ]+", "", issue_title)
return "issue-" + "-".join(sanitized.lower().split())
from . import RepositoryConfig, secrets, solve_issues_in_repository
from .gitea_client import GiteaClient
from .seen_issues_db import SeenIssuesDB
logger = logging.getLogger(__name__)
AIDER_TEST="pytest test"
AIDER_LINT="ruff format; ruff check --fix --ignore RUF022 --ignore PGH004; ruff format; ruff check --ignore RUF022 --ignore PGH004;"
MODEL = 'o3-mini'
SECRETS = secret_loader.SecretLoader()
LLM_API_KEY = SECRETS.load_or_fail('LLM_API_KEY')
GITEA_TOKEN = SECRETS.load_or_fail('GITEA_TOKEN')
def create_aider_command(issue: str) -> list[str]:
return [
'aider',
'--model', MODEL,
'--chat-language', 'english',
'--test-cmd', AIDER_TEST,
'--lint-cmd', AIDER_LINT,
'--auto-test',
'--no-auto-lint',
'--api-key', LLM_API_KEY,
'--read', 'CONVENTIONS.md',
'--message', issue,
'--yes-always',
'--architect',
]
class GiteaClient:
def __init__(self, gitea_url: str, token: str) -> None:
assert not gitea_url.endswith('/api/v1')
self.gitea_url = gitea_url + '/api/v1'
self.session = requests.Session()
self.session.headers["Content-Type"] = "application/json"
if token:
self.session.headers["Authorization"] = f"token {token}"
def get_default_branch_sha(self, owner, repo, branch):
"""Retrieve the commit SHA of the default branch."""
url = f"{self.gitea_url}/repos/{owner}/{repo}/branches/{branch}"
response = self.session.get(url)
response.raise_for_status()
data = response.json()
return data['commit']['sha']
def create_branch(self, owner, repo, new_branch, sha):
"""Create a new branch from the provided SHA."""
url = f"{self.gitea_url}/repos/{owner}/{repo}/git/refs"
json_data = {"ref": f"refs/heads/{new_branch}", "sha": sha}
response = self.session.post(url, json=json_data)
if response.status_code == 422:
logger.warning(f"Branch {new_branch} already exists.")
return False
response.raise_for_status()
return True
def get_issues(self, owner, repo):
"""Download issues from the specified repository."""
url = f"{self.gitea_url}/repos/{owner}/{repo}/issues"
response = self.session.get(url)
response.raise_for_status()
return response.json()
def create_pull_request(self, owner, repo, title, head, base, body):
"""Create a pull request for the given branch."""
url = f"{self.gitea_url}/repos/{owner}/{repo}/pulls"
json_data = {"title": title, "head": head, "base": base, "body": body}
response = self.session.post(url, json=json_data)
response.raise_for_status()
return response.json()
def parse_args():
parser = argparse.ArgumentParser(description="Download issues and create pull requests for a Gitea repository.")
parser.add_argument("--gitea-url", required=True, help="Base URL for the Gitea instance, e.g., https://gitfub.space/api/v1")
parser.add_argument("--owner", required=True, help="Owner of the repository")
parser.add_argument("--repo", required=True, help="Repository name")
parser.add_argument("--base-branch", default="main", help="Base branch to use for new branches (default: main)")
parser = argparse.ArgumentParser(
description='Download issues and create pull requests for a Gitea repository.',
)
parser.add_argument(
'--gitea-url',
required=True,
help='Base URL for the Gitea instance, e.g., https://gitfub.space/api/v1',
)
parser.add_argument('--owner', required=True, help='Owner of the repository')
parser.add_argument(
'--repo',
help='Repository name. If not specified, all repositories for the owner will be scanned.',
)
parser.add_argument(
'--base-branch',
default='main',
help='Base branch to use for new branches (default: main)',
)
parser.add_argument(
'--daemon',
action='store_true',
help='Run in daemon mode to continuously monitor for new issues',
)
parser.add_argument(
'--interval',
type=int,
default=300,
help='Interval in seconds between checks in daemon mode (default: 300)',
)
return parser.parse_args()
def run_cmd(cmd: list[str], cwd:Path|None=None) -> None:
print(cmd)
subprocess.run(cmd, check=True, cwd=cwd)
def process_issue(args, tmpdirname: Path, branch_name: str, issue_title: str, issue_description: str, issue_number: str):
repo_url = f"{args.gitea_url}:{args.owner}/{args.repo}.git".replace('https://', 'git@')
run_cmd(["git", "clone", repo_url, tmpdirname])
run_cmd(["git", "checkout", args.base_branch], tmpdirname)
run_cmd(["git", "checkout", "-b", branch_name], tmpdirname)
run_cmd(create_aider_command(f'# {issue_title}\n{issue_description}'), tmpdirname)
run_cmd(["git", "add", "."], tmpdirname)
run_cmd(["git", "push", "origin", branch_name], tmpdirname)
def main():
logging.basicConfig(level='INFO')
args = parse_args()
client = GiteaClient(args.gitea_url, GITEA_TOKEN )
seen_issues_db = SeenIssuesDB()
client = GiteaClient(args.gitea_url, secrets.gitea_token())
try:
issues = client.get_issues(args.owner, args.repo)
except Exception:
logger.exception('Failed to retrieve issues')
sys.exit(1)
if args.repo:
repositories = [args.repo]
else:
repositories = list(client.iter_user_repositories(args.owner, True))
if not issues:
logger.info("No issues found.")
return
while True:
logger.info('Checking for new issues...')
for repo in repositories:
repository_config = RepositoryConfig(
gitea_url=args.gitea_url,
owner=args.owner,
repo=repo,
base_branch=args.base_branch,
)
solve_issues_in_repository(repository_config, client, seen_issues_db)
del repo
if not args.daemon:
break
logger.info('Sleeping for %d seconds...', args.interval)
time.sleep(args.interval)
for issue in issues:
issue_number = issue.get("number")
issue_description = issue.get("body", "")
title = issue.get("title", f"Issue {issue_number}")
branch_name = generate_branch_name(title)
try:
with tempfile.TemporaryDirectory() as tmpdirname:
process_issue(args, Path(tmpdirname), branch_name, title, issue_description, issue_number)
logger.info(f"Created branch {branch_name} for issue {issue_number}.")
except Exception:
logger.exception('Error processing issue')
sys.exit(1)
body = f"Automatically generated pull request for issue: {issue.get('html_url', 'unknown')}"
try:
pr = client.create_pull_request(args.owner, args.repo, f"[Issue {issue_number}] {title}", branch_name, args.base_branch, body)
logger.info(f"Created pull request: {pr.get('html_url', 'unknown')} for issue {issue_number}.")
except Exception:
logger.exception('"Error creating pull request for branch')
sys.exit(1)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

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

170
aider_gitea/gitea_client.py Normal file
View File

@ -0,0 +1,170 @@
import logging
from collections.abc import Iterator
import requests
logger = logging.getLogger(__name__)
class GiteaClient:
"""Client for interacting with the Gitea API.
This class provides methods to interact with a Gitea instance's API,
including retrieving repository information, creating branches, and fetching issues.
Read more about the Gitea API here: https://gitea.com/api/swagger
Attributes:
gitea_url (str): The base URL for the Gitea API endpoints.
session (requests.Session): HTTP session for making API requests.
"""
def __init__(self, gitea_url: str, token: str) -> None:
"""Initialize a new Gitea API client.
Args:
gitea_url (str): Base URL for the Gitea instance (without '/api/v1').
token (str): Authentication token for the Gitea API. If empty, requests will be unauthenticated.
Raises:
AssertionError: If gitea_url ends with '/api/v1'.
"""
assert not gitea_url.endswith('/api/v1')
self.gitea_url = gitea_url + '/api/v1'
self.session = requests.Session()
self.session.headers['Content-Type'] = 'application/json'
if token:
self.session.headers['Authorization'] = f'token {token}'
def get_default_branch_sha(self, owner: str, repo: str, branch: str) -> str:
"""Retrieve the commit SHA of the specified branch.
Args:
owner (str): Owner of the repository.
repo (str): Name of the repository.
branch (str): Name of the branch.
Returns:
str: The commit SHA of the specified branch.
Raises:
requests.HTTPError: If the API request fails.
"""
url = f'{self.gitea_url}/repos/{owner}/{repo}/branches/{branch}'
response = self.session.get(url)
response.raise_for_status()
data = response.json()
return data['commit']['sha']
def create_branch(self, owner: str, repo: str, new_branch: str, sha: str) -> bool:
"""Create a new branch from the provided SHA.
Args:
owner (str): Owner of the repository.
repo (str): Name of the repository.
new_branch (str): Name of the new branch to create.
sha (str): Commit SHA to use as the starting point for the new branch.
Returns:
bool: True if the branch was created successfully, False if the branch already exists.
Raises:
requests.HTTPError: If the API request fails for reasons other than branch already existing.
"""
url = f'{self.gitea_url}/repos/{owner}/{repo}/git/refs'
json_data = {'ref': f'refs/heads/{new_branch}', 'sha': sha}
response = self.session.post(url, json=json_data)
if response.status_code == 422:
logger.warning('Branch %s already exists.', new_branch)
return False
response.raise_for_status()
return True
def get_issues(self, owner: str, repo: str) -> list[dict[str, str]]:
"""Download issues from the specified repository and filter those with the 'aider' label.
Args:
owner (str): Owner of the repository.
repo (str): Name of the repository.
Returns:
list: A list of issue dictionaries, filtered to only include issues with the 'aider' label.
Raises:
requests.HTTPError: If the API request fails.
"""
url = f'{self.gitea_url}/repos/{owner}/{repo}/issues'
response = self.session.get(url)
response.raise_for_status()
issues = response.json()
# Filter to only include issues marked with the "aider" label.
issues = [
issue
for issue in issues
if any(label.get('name') == 'aider' for label in issue.get('labels', []))
]
return issues
def iter_user_repositories(
self,
owner: str,
only_those_with_issues: bool = False,
) -> Iterator[str]:
"""Get a list of repositories for a given user.
Args:
owner (str): The owner of the repositories.
only_those_with_issues (bool): If True, only return repositories with issues enabled.
Returns:
Iterator[str]: An iterator of repository names.
"""
url = f'{self.gitea_url}/user/repos'
response = self.session.get(url)
response.raise_for_status()
for repo in response.json():
if only_those_with_issues and not repo['has_issues']:
continue
if repo['owner']['login'].lower() != owner.lower():
continue
yield repo['name']
def create_pull_request(
self,
owner: str,
repo: str,
title: str,
body: str,
head: str,
base: str,
labels: list[str] = None,
) -> dict:
"""Create a pull request and optionally apply labels.
Args:
owner (str): Owner of the repository.
repo (str): Name of the repository.
title (str): Title of the pull request.
body (str): Description/body of the pull request.
head (str): The name of the branch where changes are implemented.
base (str): The name of the branch you want the changes pulled into.
labels (list[str], optional): List of label names to apply to the pull request.
Returns:
dict: The created pull request data.
Raises:
requests.HTTPError: If the API request fails.
"""
url = f'{self.gitea_url}/repos/{owner}/{repo}/pulls'
json_data = {
'title': title,
'body': body,
'head': head,
'base': base,
}
response = self.session.post(url, json=json_data)
response.raise_for_status()
return response.json()

11
aider_gitea/secrets.py Normal file
View File

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

View File

@ -0,0 +1,123 @@
"""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 information about seen issues and their associated pull requests for efficient lookup.
"""
import sqlite3
DEFAULT_DB_PATH = 'output/seen_issues.db'
class SeenIssuesDB:
"""Database handler for tracking processed issues and pull requests.
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
"""
def __init__(self, db_path=DEFAULT_DB_PATH):
"""Initialize the database connection.
Args:
db_path: Path to the SQLite database file. Defaults to 'output/seen_issues.db'.
"""
self.conn = sqlite3.connect(db_path)
self._create_table()
def _create_table(self):
"""Create the seen_issues table if it doesn't exist.
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_url TEXT PRIMARY KEY,
issue_number TEXT,
pr_number TEXT,
pr_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
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 along with pull request information.
If the issue has already been marked as seen, this operation has no effect.
Args:
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.
"""
with self.conn:
self.conn.execute(
'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_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_url: The text content of the issue to check.
Returns:
True if the issue has been seen before, False otherwise.
"""
cursor = self.conn.execute(
'SELECT 1 FROM seen_issues WHERE issue_url = ?',
(issue_url,),
)
return cursor.fetchone() is not None
def get_pr_info(self, issue_url: str) -> tuple[str, str] | None:
"""Get pull request information for an issue.
Args:
issue_url: The text content of the issue to check.
Returns:
A tuple containing (pr_number, pr_url) if found, None otherwise.
"""
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

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
secret_loader @ git+https://gitfub.space/Jmaa/secret_loader.git

View File

@ -11,10 +11,77 @@ from setuptools import setup
PACKAGE_NAME = 'aider_gitea'
PACKAGE_DESCRIPTION = """
Aider Gitea.
A code automation tool that integrates Gitea with Aider 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.
3. Runs tests and code quality checks.
4. Creates a pull request with the solution.
## Usage
An application token must be supplied for the `gitea_token` secret. This must
have the following permissions:
- `read:issue`: To be able to read issues on the specified repository.
- `write:repository`: To be able to create pull requests.
- `read:user`: Needed to iterate all user's repositories.
### Command Line
```bash
# Run with default settings
python -m aider_gitea
# Specify custom repository and owner
python -m aider_gitea --owner myorg --repo myproject
# Use a custom Gitea URL
python -m aider_gitea --gitea-url https://gitea.example.com
# Specify a different base branch
python -m aider_gitea --base-branch develop
```
### Python API
```python
from aider_gitea import solve_issue_in_repository
from pathlib import Path
# Solve an issue programmatically
args = argparse.Namespace(
gitea_url="https://gitea.example.com",
owner="myorg",
repo="myproject",
base_branch="main"
)
solve_issue_in_repository(
args,
Path("/path/to/repo"),
"issue-123-fix-bug",
"Fix critical bug",
"The application crashes when processing large files",
"123"
)
```
### Environment Configuration
The tool uses environment variables for sensitive information:
- `GITEA_TOKEN`: Your Gitea API token
- `LLM_API_KEY`: API key for the language model used by Aider
```
""".strip()
PACKAGE_DESCRIPTION_SHORT = """
""".strip()
A code automation tool that integrates Gitea with Aider to automatically solve issues.""".strip()
def parse_version_file(text: str) -> str:
@ -29,7 +96,9 @@ with open(PACKAGE_NAME + '/_version.py') as f:
version = parse_version_file(f.read())
REQUIREMENTS_MAIN = []
REQUIREMENTS_MAIN = [
'secret_loader @ git+https://gitfub.space/Jmaa/secret_loader.git',
]
REQUIREMENTS_TEST = []

1
test/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Test package for aider_gitea."""

View File

@ -0,0 +1,18 @@
from aider_gitea import generate_branch_name
def test_generate_branch_name_normal():
# Normal case with alphanumeric title.
branch = generate_branch_name('123', 'Some Issue Title')
assert branch == 'issue-123-some-issue-title'
def test_generate_branch_name_special_characters():
branch = generate_branch_name('45', 'Issue @ Special!')
assert branch == 'issue-45-issue-special'
def test_generate_branch_name_numeric_title():
# Test where the title starts with numbers.
branch = generate_branch_name('789', '123 Numbers Here')
assert branch == 'issue-789-123-numbers-here'

View File

@ -0,0 +1,50 @@
from unittest.mock import MagicMock, patch
from aider_gitea.gitea_client import GiteaClient
class TestGiteaClientPRLabels:
def setup_method(self):
self.client = GiteaClient('https://gitea.example.com', 'fake_token')
@patch('requests.Session.post')
def test_create_pull_request_with_labels(self, mock_post):
# Mock the PR creation response
pr_response = MagicMock()
pr_response.status_code = 201
expected_result = {
'number': 123,
'title': 'Test PR',
'html_url': 'https://gitea.example.com/owner/repo/pulls/123',
}
pr_response.json.return_value = expected_result
# Mock the label addition response
label_response = MagicMock()
label_response.status_code = 200
# Set up the mock to return different responses for different calls
mock_post.side_effect = [pr_response, label_response]
# Call the method with labels
result = self.client.create_pull_request(
owner='owner',
repo='repo',
title='Test PR',
body='Test body',
head='feature-branch',
base='main',
labels=['aider'],
)
# Verify PR creation call
assert mock_post.call_count == 1
pr_call_args = mock_post.call_args_list[0]
assert (
pr_call_args[0][0]
== 'https://gitea.example.com/api/v1/repos/owner/repo/pulls'
)
assert pr_call_args[1]['json']['title'] == 'Test PR'
# Verify the result
assert result == expected_result

View File

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

@ -1,4 +1,3 @@
def test_init():
import aider_gitea # noqa: F401
import aider_gitea.secrets # noqa: F401

View File

@ -0,0 +1,16 @@
from aider_gitea.seen_issues_db import SeenIssuesDB
class TestSeenIssuesDB:
def setup_method(self):
self.db = SeenIssuesDB(':memory:')
def test_mark_and_check_seen_issue(self):
issue_text = 'Test issue'
assert not self.db.has_seen(issue_text)
self.db.mark_as_seen(issue_text)
assert self.db.has_seen(issue_text)
def test_unseen_issue(self):
issue_text = 'Unseen issue'
assert not self.db.has_seen(issue_text)

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

@ -0,0 +1,101 @@
from pathlib import Path
from unittest.mock import MagicMock, patch
from aider_gitea import IssueResolution, RepositoryConfig, solve_issue_in_repository
REPOSITORY_CONFIG = RepositoryConfig(
gitea_url='https://gitea.example.com',
owner='test-owner',
repo='test-repo',
base_branch='main',
)
class TestSolveIssueInRepository:
def setup_method(self):
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 = IssueResolution(
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,
), # git diff with changes
]
# Call the function
result = solve_issue_in_repository(
REPOSITORY_CONFIG,
self.tmpdirname,
self.branch_name,
self.issue_title,
self.issue_description,
self.issue_number,
self.gitea_client,
)
# Verify results
assert result.success 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_push_changes.return_value = IssueResolution(False, None, None)
# 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(
REPOSITORY_CONFIG,
self.tmpdirname,
self.branch_name,
self.issue_title,
self.issue_description,
self.issue_number,
self.gitea_client,
)
# Verify results
assert result.success is False
assert mock_push_changes.call_count == 0 # push_changes should not be called