Compare commits
439 Commits
basicthink
...
scripts
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c3de57a2b3 | ||
![]() |
f2f0a10a9e | ||
![]() |
607ce67792 | ||
![]() |
42c4386562 | ||
![]() |
0be6ca7e33 | ||
![]() |
21fe8dd34f | ||
![]() |
7262e03ccf | ||
![]() |
07d916c831 | ||
![]() |
5ebcb808a3 | ||
![]() |
ceff998daf | ||
![]() |
05683a04b4 | ||
![]() |
2dc0f049d8 | ||
![]() |
d40cc0f34b | ||
![]() |
46f80573ad | ||
![]() |
1de60c73c9 | ||
![]() |
433daf1603 | ||
![]() |
946d9dcf25 | ||
![]() |
cceb4c1dfb | ||
![]() |
7353fe1df4 | ||
![]() |
4dcdcc65e6 | ||
![]() |
1538d658d8 | ||
![]() |
7e15ab24cd | ||
![]() |
99b7aca0fc | ||
![]() |
344fbff719 | ||
![]() |
1c0e5e045f | ||
![]() |
2a8accc06a | ||
![]() |
8d0b75a86d | ||
![]() |
4fdd536226 | ||
![]() |
9490359c37 | ||
![]() |
e8d960794c | ||
![]() |
1cd1ac0c88 | ||
![]() |
06d707575e | ||
![]() |
e786d16bbb | ||
![]() |
99b55db6ff | ||
![]() |
44e9b7bd26 | ||
![]() |
083c714ac0 | ||
![]() |
5a0340e8e9 | ||
![]() |
021efac1d9 | ||
![]() |
bb0ed153c7 | ||
![]() |
389ad70c2e | ||
![]() |
6b04c3de11 | ||
![]() |
e591361e69 | ||
![]() |
bb1afee665 | ||
![]() |
f6c4d52b90 | ||
![]() |
e36da6da6c | ||
![]() |
c307e1ae43 | ||
![]() |
1f158fb047 | ||
![]() |
c59cd3fcb8 | ||
![]() |
b5aa4b22ac | ||
![]() |
316567c83e | ||
![]() |
2f24253997 | ||
![]() |
90d7e0b7b9 | ||
![]() |
f4a4dbc529 | ||
![]() |
656c2287e4 | ||
![]() |
d1321f4863 | ||
![]() |
ed9898d038 | ||
![]() |
5da21b00d5 | ||
![]() |
b8e2a0bef8 | ||
![]() |
014579d557 | ||
![]() |
6d34ceb8e2 | ||
![]() |
b0e7369929 | ||
![]() |
2c1a10c4ce | ||
![]() |
41a1de7bbb | ||
![]() |
5585ff9fc2 | ||
![]() |
2525010384 | ||
![]() |
a4942d8527 | ||
![]() |
ced0e1b111 | ||
![]() |
b3b01a31ba | ||
![]() |
e03d1eb5dd | ||
![]() |
1a7a272d08 | ||
![]() |
a4005b6326 | ||
![]() |
4a3b29c15e | ||
![]() |
7c30ca2135 | ||
![]() |
c47261b06f | ||
![]() |
4e38d7785b | ||
![]() |
c4352d8486 | ||
![]() |
3980e6f996 | ||
![]() |
f9fe89c5c0 | ||
![]() |
6ef65bc3ca | ||
![]() |
7f0b440e42 | ||
![]() |
d218bf7711 | ||
![]() |
8830931562 | ||
![]() |
ecdee052aa | ||
![]() |
8a15ea83f2 | ||
![]() |
d8d1c34406 | ||
![]() |
89d19679f5 | ||
![]() |
9d07b91a3e | ||
![]() |
a8b6b4ea63 | ||
![]() |
7b19535fd7 | ||
![]() |
5f4ebe5a5c | ||
![]() |
eb08230b60 | ||
![]() |
bb3d04d7a9 | ||
![]() |
8cbb645fa8 | ||
![]() |
da9f21e1dc | ||
![]() |
3a15d11026 | ||
![]() |
5595097aed | ||
![]() |
fa308c36ab | ||
![]() |
bf6e266b87 | ||
![]() |
780fe10a98 | ||
![]() |
df15fba131 | ||
![]() |
060900a86c | ||
![]() |
a3706c79f8 | ||
![]() |
95bd6d02f1 | ||
![]() |
b1a2fa39b6 | ||
![]() |
eecad6f3dd | ||
![]() |
79e6e80d82 | ||
![]() |
379f356d15 | ||
![]() |
f0e33f9e74 | ||
![]() |
619ac6532a | ||
![]() |
1146444fbd | ||
![]() |
1a303b4b0c | ||
![]() |
b0fe0b4be7 | ||
![]() |
094e715f79 | ||
![]() |
264912f3f1 | ||
![]() |
4ead9f6b67 | ||
![]() |
e008f17af1 | ||
![]() |
62b450d677 | ||
![]() |
14b3197543 | ||
![]() |
24bd31288a | ||
![]() |
813ae0f59d | ||
![]() |
d0a0a3e090 | ||
![]() |
d6c0ce7b40 | ||
![]() |
a7566e5055 | ||
![]() |
ec187a0c1b | ||
![]() |
e883bf6f4f | ||
![]() |
6091f1c149 | ||
![]() |
da12ac1fb9 | ||
![]() |
4b6827d0f9 | ||
![]() |
2f3a423741 | ||
![]() |
60c4c3d882 | ||
![]() |
adc78c431b | ||
![]() |
ccf6ba3d1a | ||
![]() |
71a8def4c4 | ||
![]() |
62bbd1ac29 | ||
![]() |
b8a44d2f67 | ||
![]() |
bbc53e488e | ||
![]() |
ff37ed592c | ||
![]() |
634b3a7d63 | ||
![]() |
fa8058b5ff | ||
![]() |
53dc01015f | ||
![]() |
cd7b56b4fe | ||
![]() |
c212e704a1 | ||
![]() |
5bf05e04a5 | ||
![]() |
1c47b32987 | ||
![]() |
5c6395adcc | ||
![]() |
fa04ed820e | ||
![]() |
ea5288e4f1 | ||
![]() |
c4a206bbe6 | ||
![]() |
a9015f2a37 | ||
![]() |
c770b55a6e | ||
![]() |
3b3fcaeef9 | ||
![]() |
75de378f66 | ||
![]() |
0805ab223e | ||
![]() |
784fec4238 | ||
![]() |
e45956edde | ||
![]() |
13ac7c466a | ||
![]() |
1b4540a7a8 | ||
![]() |
0d610cfb31 | ||
![]() |
9ccec5058e | ||
![]() |
fdf98281ea | ||
![]() |
c905c7c850 | ||
![]() |
b3cc1eb54f | ||
![]() |
ec518753c0 | ||
![]() |
f596f7729d | ||
![]() |
d69f270262 | ||
![]() |
f7a47915c8 | ||
![]() |
a867c23be1 | ||
![]() |
55e30a9592 | ||
![]() |
25572af366 | ||
![]() |
17d55bd324 | ||
![]() |
dabd12f5cf | ||
![]() |
b87414e38d | ||
![]() |
8b33eaa947 | ||
![]() |
d81c33d1da | ||
![]() |
b63e48c8a4 | ||
![]() |
704f2e3b92 | ||
![]() |
d9967f91ce | ||
![]() |
6a969358af | ||
![]() |
db3fe4cbc6 | ||
![]() |
ec374b6250 | ||
![]() |
daea79e719 | ||
![]() |
ec0103eaf3 | ||
![]() |
b7d36a3b7a | ||
![]() |
686c355b98 | ||
![]() |
efbe2ca507 | ||
![]() |
bb0a5e3d04 | ||
![]() |
507cf9e3c1 | ||
![]() |
a7deccc441 | ||
![]() |
1d57a9424e | ||
![]() |
612db5fa6a | ||
![]() |
e0627bf780 | ||
![]() |
fc192d3de6 | ||
![]() |
ae9fca1442 | ||
![]() |
2eb2d8abb0 | ||
![]() |
2272ab55bd | ||
![]() |
ddc236f297 | ||
![]() |
b66cfc9a4f | ||
![]() |
4938eeb74c | ||
![]() |
86b81459e0 | ||
![]() |
574dbaf9ba | ||
![]() |
025858a231 | ||
![]() |
c1ce229c64 | ||
![]() |
09d23d34a8 | ||
![]() |
5395639347 | ||
![]() |
852706d25d | ||
![]() |
ceb280012d | ||
![]() |
3a0600ae9e | ||
![]() |
e3cb0725a0 | ||
![]() |
ad4c2310d3 | ||
![]() |
e9cac56372 | ||
![]() |
70c926166a | ||
![]() |
cc61a3e600 | ||
![]() |
355686d280 | ||
![]() |
8ea2e832bc | ||
![]() |
e190aeda25 | ||
![]() |
1219dc8fd1 | ||
![]() |
84c7596837 | ||
![]() |
9dead9b3cf | ||
![]() |
6c9bf72a13 | ||
![]() |
1ef0bff393 | ||
![]() |
0da8287ad9 | ||
![]() |
1896c2b933 | ||
![]() |
bee6f21535 | ||
![]() |
ef4ea025a9 | ||
![]() |
f753fb7bb2 | ||
![]() |
ef6554df5a | ||
![]() |
4fb3b79ebe | ||
![]() |
e7b3a66ab6 | ||
![]() |
6a1fdc829e | ||
![]() |
ccc1d97c90 | ||
![]() |
c44891a39f | ||
![]() |
3b1b24e1b2 | ||
![]() |
4b756f7908 | ||
![]() |
48441e2514 | ||
![]() |
bfa4198ba2 | ||
![]() |
874da1710f | ||
![]() |
7981a7e5bb | ||
![]() |
079faae3a6 | ||
![]() |
a55565161c | ||
![]() |
50ed9b35d1 | ||
![]() |
991267e9d6 | ||
![]() |
8d32101c80 | ||
![]() |
db6b2f4b4c | ||
![]() |
1d9cc2f85a | ||
![]() |
f13ae6530b | ||
![]() |
fccfcafe98 | ||
![]() |
0196eb019f | ||
![]() |
9107b1e963 | ||
![]() |
77dead7745 | ||
![]() |
26d2a91192 | ||
![]() |
ebbabe321c | ||
![]() |
79139fdcc8 | ||
![]() |
01e57e6e48 | ||
![]() |
f3a1cd4dee | ||
![]() |
06074e744c | ||
![]() |
7938975753 | ||
![]() |
0addeb462e | ||
![]() |
cf8ecc7d2f | ||
![]() |
49d3efa5b7 | ||
![]() |
e320c04eb1 | ||
![]() |
8d0d1e4f86 | ||
![]() |
d8fe05f4ff | ||
![]() |
95c29f8aac | ||
![]() |
2c7dc280e6 | ||
![]() |
18d3c1369e | ||
![]() |
b903d0fe03 | ||
![]() |
f20c258833 | ||
![]() |
1f541cd4c6 | ||
![]() |
84cc241a75 | ||
![]() |
91cea1b59c | ||
![]() |
62c60a1386 | ||
![]() |
d66ada970b | ||
![]() |
9e84e88677 | ||
![]() |
c6b9ad3e44 | ||
![]() |
ef1c9ef3cb | ||
![]() |
5f1926fcdd | ||
![]() |
9efc523577 | ||
![]() |
2288eb480b | ||
![]() |
c31817f44a | ||
![]() |
8d302b1533 | ||
![]() |
82d3fd5055 | ||
![]() |
ea80e1c4a4 | ||
![]() |
98e3219bb1 | ||
![]() |
5a8704a58d | ||
![]() |
366b17d092 | ||
![]() |
f8d8745231 | ||
![]() |
7b6bc1a7bc | ||
![]() |
7d639f93f4 | ||
![]() |
b271a13f66 | ||
![]() |
dac115ed32 | ||
![]() |
173fa768e5 | ||
![]() |
9a3325469a | ||
![]() |
48b8590ea0 | ||
![]() |
212537b8a2 | ||
![]() |
997c3f86dc | ||
![]() |
162c545b74 | ||
![]() |
3e93883c4f | ||
![]() |
0b90fe4032 | ||
![]() |
b3c0810e54 | ||
![]() |
bde44aa50e | ||
![]() |
bc44a69125 | ||
![]() |
6f737020fc | ||
![]() |
bcba74375b | ||
![]() |
c83058cbea | ||
![]() |
5d156e55b2 | ||
![]() |
e8ab8c46fd | ||
![]() |
15735d7aee | ||
![]() |
2157a823fb | ||
![]() |
294c2d1574 | ||
![]() |
dae577bd0b | ||
![]() |
db9c46d1bf | ||
![]() |
4a3dfc848f | ||
![]() |
340c8cbfa9 | ||
![]() |
491a0d1d77 | ||
![]() |
847576f902 | ||
![]() |
be63f25f90 | ||
![]() |
d0fc2a453e | ||
![]() |
6f1b5ca2ef | ||
![]() |
47887ca6f9 | ||
![]() |
9aa9c44934 | ||
![]() |
f0ed2a64ee | ||
![]() |
b81624c764 | ||
![]() |
380e727e61 | ||
![]() |
3d3241f282 | ||
![]() |
c805239183 | ||
![]() |
dbdb1ba29d | ||
![]() |
d8866a5182 | ||
![]() |
cd43eb373f | ||
![]() |
6aaeac784f | ||
![]() |
af54106813 | ||
![]() |
164156bdc0 | ||
![]() |
20a94c78f5 | ||
![]() |
a66d2d33a3 | ||
![]() |
00f0fa1343 | ||
![]() |
685449821f | ||
![]() |
e795e4853e | ||
![]() |
d0020e838b | ||
![]() |
a5686e759d | ||
![]() |
997e631e6c | ||
![]() |
7c9c9d6574 | ||
![]() |
a0ce83b21c | ||
![]() |
04c7e6818b | ||
![]() |
eb7e756877 | ||
![]() |
1974cfea38 | ||
![]() |
1c6f04f6d2 | ||
![]() |
5f0198d52c | ||
![]() |
6330983635 | ||
![]() |
4d1fc2fffb | ||
![]() |
6bf05f9b7d | ||
![]() |
8f46b05be9 | ||
![]() |
c0f76e6811 | ||
![]() |
a8969b0038 | ||
![]() |
254ddc9372 | ||
![]() |
c584b186e0 | ||
![]() |
70d9db5084 | ||
![]() |
9979f0c668 | ||
![]() |
9c1bdf8053 | ||
![]() |
c0c26094ff | ||
![]() |
0ee7015758 | ||
![]() |
4f03a00910 | ||
![]() |
e929049b68 | ||
![]() |
08b2246775 | ||
![]() |
bca2507c2f | ||
![]() |
f90ef63606 | ||
![]() |
56aedf8793 | ||
![]() |
13fe016f21 | ||
![]() |
05d8e6dda7 | ||
![]() |
7d5887e98c | ||
![]() |
57da9e9841 | ||
![]() |
69e178e7a7 | ||
![]() |
21217d2bb7 | ||
![]() |
c5d4272cbb | ||
![]() |
7db4a678f4 | ||
![]() |
7d3594c928 | ||
![]() |
824fa014b2 | ||
![]() |
5ab2749aa8 | ||
![]() |
0bc33d8bf8 | ||
![]() |
a64c881815 | ||
![]() |
c4d6e82ae9 | ||
![]() |
8b12669505 | ||
![]() |
d2160b6736 | ||
![]() |
05cce75127 | ||
![]() |
278ca673b7 | ||
![]() |
59e05c7640 | ||
![]() |
4704f51be1 | ||
![]() |
28123663de | ||
![]() |
fda4ad2a02 | ||
![]() |
3be880eabd | ||
![]() |
40934b8869 | ||
![]() |
75ff48c6bc | ||
![]() |
925c0890c1 | ||
![]() |
ed03cbf08a | ||
![]() |
6305c8347a | ||
![]() |
9246d4d32d | ||
![]() |
8459e69713 | ||
![]() |
53cbca5c69 | ||
![]() |
0b837ff045 | ||
![]() |
ae1a3bb400 | ||
![]() |
d496d6b9de | ||
![]() |
005f1fae7b | ||
![]() |
d19faf4f5c | ||
![]() |
d53433d618 | ||
![]() |
05625059d5 | ||
![]() |
8ac6fb3c6a | ||
![]() |
64192e0b2c | ||
![]() |
36fc46212a | ||
![]() |
e10817375f | ||
![]() |
83f6922f63 | ||
![]() |
b3eb8c6670 | ||
![]() |
4ac2167146 | ||
![]() |
f5b430e712 | ||
![]() |
c83b273e64 | ||
![]() |
328af00e41 | ||
![]() |
a28b3d676b | ||
![]() |
6e7c9e7bed | ||
![]() |
02a0f871e0 | ||
![]() |
d99a05ebc9 | ||
![]() |
5d1ddda356 | ||
![]() |
d4e262dfd9 | ||
![]() |
7fbf44cd06 | ||
![]() |
2ec603a944 | ||
![]() |
98233481df | ||
![]() |
b5ad6f25ea | ||
![]() |
788a6f08f8 | ||
![]() |
8b6767dc7d | ||
![]() |
446f80ebad | ||
![]() |
590b744dc9 | ||
![]() |
74b933eecc | ||
![]() |
93178e521e | ||
![]() |
51aa71a0cb | ||
![]() |
6af7ff68cc | ||
![]() |
86d087dd12 | ||
![]() |
ea809d8ac1 | ||
![]() |
5891e22194 | ||
![]() |
ce6fcddd82 | ||
![]() |
47e2bd9b9b | ||
![]() |
68b4258660 | ||
![]() |
6de24f0d06 | ||
![]() |
960a8b9494 |
7
.github/workflows/ci.yaml
vendored
7
.github/workflows/ci.yaml
vendored
@ -2,9 +2,9 @@ name: Dev CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [ main, scripts ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, scripts ]
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
@ -15,9 +15,6 @@ jobs:
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11.4"
|
||||
- name: install workflows-command dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
- name: install dev dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
|
34
.github/workflows/sync-to-gitlab.yml
vendored
Normal file
34
.github/workflows/sync-to-gitlab.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: Sync to GitLab
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- scripts
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout GitHub repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config user.name "gh-action"
|
||||
git config user.email "gh-action@merico.dev"
|
||||
|
||||
- name: Add GitLab remote
|
||||
run: |
|
||||
git remote add gitlab https://oauth2:${{ secrets.GITLAB_ACCESS_TOKEN }}@gitlab.com/devchat-ai/workflows.git
|
||||
|
||||
- name: Retry Command
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: |
|
||||
git fetch gitlab &&
|
||||
git push gitlab --force $GITHUB_REF_NAME:$GITHUB_REF_NAME
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,4 +1,12 @@
|
||||
.vscode
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
tmp/
|
||||
|
||||
cache/
|
||||
|
||||
custom/*
|
||||
!custom/config.yml.example
|
||||
|
||||
user_settings.yml
|
||||
.aider*
|
||||
|
13
Makefile
13
Makefile
@ -1,20 +1,23 @@
|
||||
.PHONY: setup-dev check fix
|
||||
|
||||
div = $(shell printf '=%.0s' {1..120})
|
||||
|
||||
setup-dev:
|
||||
@echo "Setting up development environment..."
|
||||
@echo "Installing dev dependencies..."
|
||||
@~/.chat/mamba/envs/devchat-commands/bin/pip install -r requirements-dev.txt
|
||||
@pip install -r requirements-dev.txt
|
||||
@echo "Done!"
|
||||
|
||||
T="."
|
||||
check:
|
||||
@echo ${div}
|
||||
~/.chat/mamba/envs/devchat-commands/bin/python -m ruff check .
|
||||
~/.chat/mamba/envs/devchat-commands/bin/python -m ruff format . --check
|
||||
ruff check $(T)
|
||||
ruff format $(T) --check
|
||||
@echo "Done!"
|
||||
|
||||
fix:
|
||||
@echo ${div}
|
||||
~/.chat/mamba/envs/devchat-commands/bin/python -m ruff format .
|
||||
ruff format $(T)
|
||||
@echo ${div}
|
||||
~/.chat/mamba/envs/devchat-commands/bin/python -m ruff check . --fix
|
||||
ruff check $(T) --fix
|
||||
@echo "Done!"
|
||||
|
@ -1,9 +1,2 @@
|
||||
# workflows
|
||||
DevChat workflows (the `sys` directory)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```shell
|
||||
cd ~/.chat/workflows
|
||||
git clone https://github.com/devchat-ai/workflows.git sys
|
||||
```
|
||||
DevChat workflows management for workflow engine 2.0.
|
||||
|
@ -1 +0,0 @@
|
||||
description: Generate code with a general template embedded into the prompt.
|
@ -1,28 +0,0 @@
|
||||
As a software developer assistant, your tasks are to:
|
||||
|
||||
- Provide a clear and concise response to address the user's requirements.
|
||||
- Write code and give advice based on given code or information in the <context> if provided.
|
||||
- Follow language-specific best practices and common coding standards.
|
||||
|
||||
When responding:
|
||||
|
||||
1. First summarize the requirements or provided information in your own words.
|
||||
The summary should better be written in bullet points (excluding code).
|
||||
2. When modifying the provided code, include the entire modified functions, but exclude any unmodified functions.
|
||||
If any global statements are changed, include the full global statements; otherwise, do not include them.
|
||||
3. Enclose code or changes within blocks using triple backticks (```), and include the programming language and the file path.
|
||||
For example:
|
||||
```python path=./path/to/file.py
|
||||
print("Hello, World!")
|
||||
```
|
||||
Do your best to deduce the file path based on the given <context> or previous messages.
|
||||
If you are still uncertain about the file path of the code, feel free to omit it.
|
||||
4. Use separate code blocks for different files.
|
||||
5. When providing a suggestion or instruction, begin by explaining the reason behind it.
|
||||
6. You may not receive all the direct information needed for your task.
|
||||
Analyze the given <context> to understand how existing code was written, and use this knowledge for your task.
|
||||
7. Note that not all previous messages or contexts are necessarily relevant.
|
||||
8. Respond in the language of the request.
|
||||
|
||||
You may encounter duplicate or conflicting messages or contexts, and the later ones should be considered as the most accurate.
|
||||
If you need more information, ask for it.
|
@ -1 +0,0 @@
|
||||
description: Generate code with a Python-specific template embedded into the prompt.
|
@ -1,5 +0,0 @@
|
||||
When writing Python code, maintain compliance with PEP-8 guidelines:
|
||||
- Include type hints where appropriate.
|
||||
- Provide docstrings for classes and functions.
|
||||
- Specify an encoding when opening documents. E.g., `open(file_path, encoding="utf-8")`.
|
||||
- Follow other best practices in PEP-8.
|
@ -1,5 +0,0 @@
|
||||
description: Writes a well-formatted commit message for selected code changes and commits them via Git. Include an issue number if desired (e.g., input "/commit to close #12").
|
||||
hint: to close Issue #issue_number
|
||||
input: optional
|
||||
steps:
|
||||
- run: $devchat_python $command_path/commit.py "$input" "english"
|
485
commit/commit.py
485
commit/commit.py
@ -1,485 +0,0 @@
|
||||
# flake8: noqa: E402
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "libs"))
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "libs"))
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from ui_utils import ui_checkbox_select, ui_text_edit, CheckboxOption # noqa: E402
|
||||
from llm_api import chat_completion_no_stream, chat_completion_no_stream_return_json # noqa: E402
|
||||
from ide_services.services import log_info
|
||||
|
||||
from prompts import (
|
||||
PROMPT_SUMMARY_FOR_FILES,
|
||||
PROMPT_GROUP_FILES,
|
||||
PROMPT_COMMIT_MESSAGE_BY_DIFF_USER_INPUT,
|
||||
PROMPT_COMMIT_MESSAGE_BY_SUMMARY_USER_INPUT,
|
||||
PROMPT_SUMMARY_FOR_FILES_RETRY,
|
||||
PROMPT_GROUP_FILES_RETRY,
|
||||
prompt_summary_for_files_llm_config,
|
||||
prompt_group_files_llm_config,
|
||||
prompt_commit_message_by_diff_user_input_llm_config,
|
||||
prompt_commit_message_by_summary_user_input_llm_config,
|
||||
)
|
||||
|
||||
|
||||
language = ""
|
||||
|
||||
|
||||
def assert_value(value, message):
|
||||
"""
|
||||
判断给定的value是否为True,如果是,则输出指定的message并终止程序。
|
||||
|
||||
Args:
|
||||
value: 用于判断的值。
|
||||
message: 如果value为True时需要输出的信息。
|
||||
|
||||
Returns:
|
||||
无返回值。
|
||||
|
||||
"""
|
||||
if value:
|
||||
print(message, file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def decode_path(encoded_path):
|
||||
octal_pattern = re.compile(r"\\[0-7]{3}")
|
||||
|
||||
if octal_pattern.search(encoded_path):
|
||||
bytes_path = encoded_path.encode("utf-8").decode("unicode_escape").encode("latin1")
|
||||
decoded_path = bytes_path.decode("utf-8")
|
||||
return decoded_path
|
||||
else:
|
||||
return encoded_path
|
||||
|
||||
|
||||
def get_modified_files():
|
||||
"""
|
||||
获取当前修改文件列表以及已经staged的文件列表
|
||||
|
||||
Args:
|
||||
无
|
||||
|
||||
Returns:
|
||||
tuple: 包含两个list的元组,第一个list包含当前修改过的文件,第二个list包含已经staged的文件
|
||||
"""
|
||||
""" 获取当前修改文件列表以及已经staged的文件列表"""
|
||||
output = subprocess.check_output(["git", "status", "-s", "-u"], text=True, encoding="utf-8")
|
||||
lines = output.split("\n")
|
||||
modified_files = []
|
||||
staged_files = []
|
||||
|
||||
def strip_file_name(file_name):
|
||||
file = file_name.strip()
|
||||
if file.startswith('"'):
|
||||
file = file[1:-1]
|
||||
return file
|
||||
|
||||
for line in lines:
|
||||
if len(line) > 2:
|
||||
status, filename = line[:2], decode_path(line[3:])
|
||||
# check wether filename is a directory
|
||||
if os.path.isdir(filename):
|
||||
continue
|
||||
modified_files.append(os.path.normpath(strip_file_name(filename)))
|
||||
if status == "M " or status == "A ":
|
||||
staged_files.append(os.path.normpath(strip_file_name(filename)))
|
||||
return modified_files, staged_files
|
||||
|
||||
|
||||
def gpt_file_summary(diff, diff_files, user_input):
|
||||
"""
|
||||
生成GPT对话,获取文件差异内容的摘要。
|
||||
|
||||
Args:
|
||||
diff (str): 文件差异内容。
|
||||
diff_files (List[str]): 文件差异列表。
|
||||
user_input (str): 用户输入。
|
||||
|
||||
Returns:
|
||||
dict: 文件路径作为键,摘要内容作为值的字典。
|
||||
"""
|
||||
global language
|
||||
prompt = PROMPT_SUMMARY_FOR_FILES.replace("{__DIFF__}", f"{diff}").replace(
|
||||
"{__USER_INPUT__}", f"{user_input}"
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
+ (" \nPlease response summaries in chinese" if language == "chinese" else ""),
|
||||
}
|
||||
]
|
||||
normpath_summaries = {}
|
||||
|
||||
retry_times = 0
|
||||
while retry_times < 3:
|
||||
retry_times += 1
|
||||
file_summaries = chat_completion_no_stream_return_json(
|
||||
messages, prompt_summary_for_files_llm_config
|
||||
)
|
||||
if not file_summaries:
|
||||
continue
|
||||
for key, value in file_summaries.items():
|
||||
normpath_summaries[os.path.normpath(key)] = value
|
||||
|
||||
missed_files = [file for file in diff_files if file not in normpath_summaries]
|
||||
if len(missed_files) > 0:
|
||||
prompt_retry = PROMPT_SUMMARY_FOR_FILES_RETRY.replace(
|
||||
"{__MISSED_FILES__}", f"{missed_files}"
|
||||
)
|
||||
messages.append({"role": "assistant", "content": json.dumps(file_summaries)})
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt_retry
|
||||
+ (" \nPlease response summaries in chinese" if language == "chinese" else ""),
|
||||
}
|
||||
)
|
||||
else:
|
||||
break
|
||||
|
||||
return normpath_summaries
|
||||
|
||||
|
||||
def gpt_file_group(diff, diff_files):
|
||||
"""
|
||||
根据diff和diff_files列表,对文件进行分组,返回分组结果。
|
||||
|
||||
Args:
|
||||
diff (str): 差异信息。
|
||||
diff_files (List[str]): 文件列表。
|
||||
|
||||
Returns:
|
||||
List[Dict]: 文件分组结果,每个分组是一个字典,包含"name"和"files"两个键值对,
|
||||
分别表示分组名称和该分组下的文件列表。
|
||||
"""
|
||||
prompt = PROMPT_GROUP_FILES.replace("{__DIFF__}", f"{diff}")
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
file_groups = []
|
||||
|
||||
retry_times = 0
|
||||
while retry_times < 3:
|
||||
retry_times += 1
|
||||
file_groups = chat_completion_no_stream_return_json(messages, prompt_group_files_llm_config)
|
||||
if not file_groups:
|
||||
continue
|
||||
if "groups" in file_groups:
|
||||
file_groups = file_groups["groups"]
|
||||
grouped_files = []
|
||||
for group in file_groups:
|
||||
grouped_files.extend(group["files"])
|
||||
missed_files = [file for file in diff_files if file not in grouped_files]
|
||||
|
||||
if len(missed_files) > 0:
|
||||
prompt_retry = PROMPT_GROUP_FILES_RETRY.replace("{__MISSED_FILES__}", f"{missed_files}")
|
||||
messages.append({"role": "assistant", "content": json.dumps(file_groups)})
|
||||
messages.append({"role": "user", "content": prompt_retry})
|
||||
else:
|
||||
break
|
||||
|
||||
return file_groups
|
||||
|
||||
|
||||
def get_file_summaries(modified_files, staged_files, user_input):
|
||||
"""
|
||||
计算git暂存区文件的差异,并生成对应的AI模型输入格式。
|
||||
|
||||
Args:
|
||||
modified_files (list): 当前工作区的修改文件列表
|
||||
staged_files (list): 已暂存文件列表
|
||||
user_input (str): 用户输入信息
|
||||
|
||||
Returns:
|
||||
dict: AI模型输出格式,包含normpath_summaries和modified_files两个key-value对。
|
||||
"""
|
||||
diffs = []
|
||||
for file in modified_files:
|
||||
if file not in staged_files:
|
||||
subprocess.check_output(["git", "add", file])
|
||||
diff = subprocess.check_output(["git", "diff", "--cached", file])
|
||||
if file not in staged_files:
|
||||
subprocess.check_output(["git", "reset", file])
|
||||
diffs.append(diff.decode("utf-8"))
|
||||
# total_diff = subprocess.check_output(["git", "diff", "HEAD"])
|
||||
total_diff_decoded = "\n".join(diffs) # total_diff.decode('utf-8')
|
||||
|
||||
if len(total_diff_decoded) > 15000:
|
||||
print("Current diff length:", len(total_diff_decoded), flush=True)
|
||||
return {}
|
||||
|
||||
# 在prompt中明确处置AI模型的输出格式需求
|
||||
normpath_summaries = gpt_file_summary(total_diff_decoded, modified_files, user_input)
|
||||
|
||||
return normpath_summaries
|
||||
|
||||
|
||||
def get_file_summaries_and_groups(modified_files, staged_files, user_input):
|
||||
"""
|
||||
获取已修改文件的摘要和分组。
|
||||
|
||||
Args:
|
||||
modified_files (List[str]): 已修改的文件列表。
|
||||
staged_files (List[str]): 已暂存的文件列表。
|
||||
user_input (str): 用户输入。
|
||||
|
||||
Returns:
|
||||
Tuple[Dict[str, Any], List[Dict[str, Any]]]: 包含以下两个元素的元组:
|
||||
- 文件摘要信息,字典类型,键为文件路径,值为该文件对应的摘要信息;
|
||||
- 文件分组信息,列表类型,每个元素为包含以下三个键值对的字典:
|
||||
* group_id:组ID。
|
||||
* files:属于该分组的文件列表。
|
||||
* summary:该分组的摘要信息。
|
||||
"""
|
||||
diffs = []
|
||||
for file in modified_files:
|
||||
if file not in staged_files:
|
||||
subprocess.check_output(["git", "add", file])
|
||||
diff = subprocess.check_output(["git", "diff", "--cached", file])
|
||||
if file not in staged_files:
|
||||
subprocess.check_output(["git", "reset", file])
|
||||
diffs.append(diff.decode("utf-8"))
|
||||
# total_diff = subprocess.check_output(["git", "diff", "HEAD"])
|
||||
total_diff_decoded = "\n".join(diffs) # total_diff.decode('utf-8')
|
||||
|
||||
if len(total_diff_decoded) > 15000:
|
||||
print("Current diff length:", len(total_diff_decoded), flush=True)
|
||||
return {}, []
|
||||
|
||||
# 在prompt中明确处置AI模型的输出格式需求
|
||||
normpath_summaries = gpt_file_summary(total_diff_decoded, modified_files, user_input)
|
||||
print(
|
||||
f"""
|
||||
``` file summary
|
||||
{json.dumps(normpath_summaries, indent=4)}
|
||||
```
|
||||
"""
|
||||
)
|
||||
|
||||
# 通过AI模型对提交文件进行分组,分组的依据是按修改内容的关联性。
|
||||
file_groups = gpt_file_group(total_diff_decoded, modified_files)
|
||||
print(
|
||||
f"""
|
||||
``` group
|
||||
{json.dumps(file_groups, indent=4)}
|
||||
```
|
||||
"""
|
||||
)
|
||||
|
||||
return normpath_summaries, file_groups
|
||||
|
||||
|
||||
def get_marked_files(modified_files, staged_files, file_summaries):
|
||||
"""
|
||||
根据给定的参数获取用户选中以供提交的文件
|
||||
|
||||
Args:
|
||||
modified_files (List[str]): 用户已修改文件列表
|
||||
staged_files (List[str]): 用户已staged文件列表
|
||||
file_summaries (Dict[str, str]): 文件摘要信息,key为文件名,value为摘要信息
|
||||
file_groups (List[Dict[str, Any]]): 文件分组信息,每个元素是一个字典,
|
||||
包含两个key值分别为 "importance_level" 和 "files",
|
||||
分别表示文件的重要程度和该重要程度下的文件列表
|
||||
|
||||
Returns:
|
||||
List[str]: 用户选中的文件列表
|
||||
"""
|
||||
options: List[CheckboxOption] = []
|
||||
options += [
|
||||
CheckboxOption(file, file + " - " + file_summaries.get(file, ""), "Staged", True)
|
||||
for file in staged_files
|
||||
]
|
||||
options += [
|
||||
CheckboxOption(file, file + " - " + file_summaries.get(file, ""), "Unstaged", False)
|
||||
for file in modified_files
|
||||
if file not in staged_files
|
||||
]
|
||||
|
||||
selected_files = ui_checkbox_select("Select the files you've changed that you wish to include in this commit, then click 'Submit'.", options)
|
||||
return selected_files
|
||||
|
||||
|
||||
def rebuild_stage_list(user_files):
|
||||
"""
|
||||
根据用户选中文件,重新构建stage列表
|
||||
|
||||
Args:
|
||||
user_files: 用户选中的文件列表
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# Unstage all files
|
||||
subprocess.check_output(["git", "reset"])
|
||||
# Stage all user_files
|
||||
for file in user_files:
|
||||
os.system(f'git add "{file}"')
|
||||
|
||||
|
||||
def get_diff():
|
||||
"""
|
||||
获取暂存区文件的Diff信息
|
||||
|
||||
Args:
|
||||
无
|
||||
|
||||
Returns:
|
||||
bytes: 返回bytes类型,是git diff --cached命令的输出结果
|
||||
|
||||
"""
|
||||
return subprocess.check_output(["git", "diff", "--cached"])
|
||||
|
||||
|
||||
def generate_commit_message_base_diff(user_input, diff):
|
||||
"""
|
||||
根据diff信息,通过AI生成一个commit消息
|
||||
|
||||
Args:
|
||||
user_input (str): 用户输入的commit信息
|
||||
diff (str): 提交的diff信息
|
||||
|
||||
Returns:
|
||||
str: 生成的commit消息
|
||||
|
||||
"""
|
||||
global language
|
||||
language_prompt = (
|
||||
"You must response commit message in chinese。\n" if language == "chinese" else ""
|
||||
)
|
||||
prompt = PROMPT_COMMIT_MESSAGE_BY_DIFF_USER_INPUT.replace("{__DIFF__}", f"{diff}").replace(
|
||||
"{__USER_INPUT__}", f"{user_input + language_prompt}"
|
||||
)
|
||||
if len(str(prompt)) > 10000:
|
||||
print(
|
||||
"Due to the large size of the diff data, "
|
||||
"generating a commit message through AI would be very costly, therefore, "
|
||||
"it is not recommended to use AI for generating the description. "
|
||||
"Please manually edit the commit message before submitting."
|
||||
)
|
||||
return {"content": ""}
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
response = chat_completion_no_stream(
|
||||
messages, prompt_commit_message_by_diff_user_input_llm_config
|
||||
)
|
||||
assert_value(not response, "")
|
||||
return response
|
||||
|
||||
|
||||
def generate_commit_message_base_file_summaries(user_input, file_summaries):
|
||||
"""
|
||||
根据文件摘要生成通过AI生成的提交消息
|
||||
|
||||
Args:
|
||||
user_input (str): 用户输入
|
||||
file_summaries (list[dict]): 文件摘要列表
|
||||
|
||||
Returns:
|
||||
str: 提交消息
|
||||
"""
|
||||
global language
|
||||
language_prompt = (
|
||||
"Please response commit message in chinese.\n" if language == "chinese" else ""
|
||||
)
|
||||
prompt = PROMPT_COMMIT_MESSAGE_BY_SUMMARY_USER_INPUT.replace(
|
||||
"{__USER_INPUT__}", f"{user_input}"
|
||||
).replace("{__FILE_SUMMARY__}", f"{json.dumps(file_summaries, indent=4)}")
|
||||
# Call AI model to generate commit message
|
||||
messages = [{"role": "user", "content": language_prompt + prompt}]
|
||||
response = chat_completion_no_stream(
|
||||
messages, prompt_commit_message_by_summary_user_input_llm_config
|
||||
)
|
||||
assert_value(not response, "")
|
||||
return response
|
||||
|
||||
|
||||
def display_commit_message_and_commit(commit_message):
|
||||
"""
|
||||
展示提交信息并提交。
|
||||
|
||||
Args:
|
||||
commit_message: 提交信息。
|
||||
|
||||
Returns:
|
||||
None。
|
||||
|
||||
"""
|
||||
new_commit_message = ui_text_edit("I've drafted a commit message for the code changes you selected. You can edit this message in the widget below. After confirming the message, click 'Commit', and I will proceed with the commit using this message.", commit_message)
|
||||
if not new_commit_message:
|
||||
return
|
||||
subprocess.check_output(["git", "commit", "-m", new_commit_message])
|
||||
|
||||
|
||||
def check_git_installed():
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "--version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
except FileNotFoundError:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
except Exception:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
global language
|
||||
try:
|
||||
log_info("Start commit workflow ...")
|
||||
# Ensure enough command line arguments are provided
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python script.py <user_input> <language>", file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
user_input = sys.argv[1]
|
||||
language = sys.argv[2]
|
||||
|
||||
if not check_git_installed():
|
||||
sys.exit(-1)
|
||||
|
||||
modified_files, staged_files = get_modified_files()
|
||||
if len(modified_files) == 0:
|
||||
print("No files to commit.", file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
file_summaries = get_file_summaries(modified_files, staged_files, user_input)
|
||||
selected_files = get_marked_files(modified_files, staged_files, file_summaries)
|
||||
if not selected_files:
|
||||
print("No files selected, commit aborted.")
|
||||
return
|
||||
|
||||
rebuild_stage_list(selected_files)
|
||||
summaries_for_select_files = {
|
||||
file: file_summaries[file] for file in selected_files if file in file_summaries
|
||||
}
|
||||
|
||||
if len(summaries_for_select_files.keys()) < len(selected_files):
|
||||
diff = get_diff()
|
||||
commit_message = generate_commit_message_base_diff(user_input, diff)
|
||||
else:
|
||||
commit_message = generate_commit_message_base_file_summaries(
|
||||
user_input, summaries_for_select_files
|
||||
)
|
||||
|
||||
display_commit_message_and_commit(commit_message["content"])
|
||||
sys.exit(0)
|
||||
except Exception as err:
|
||||
print("Exception:", err, file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,183 +0,0 @@
|
||||
# ruff: noqa
|
||||
|
||||
# summary changes for files based diff
|
||||
# diff => {__DIFF__}
|
||||
PROMPT_SUMMARY_FOR_FILES = """
|
||||
Objective: **Create concise summaries for each modified file based on the provided diff changes and any additional user input.**
|
||||
|
||||
**Instructions:**
|
||||
1. Review the diff changes and user input to understand the context and content of the modifications.
|
||||
2. Write a summary for each file that has been modified, capturing the essence of the changes.
|
||||
3. Use the filename from the diff as the key, and the summary as the value in the output JSON object.
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
\"filename1 with path\": \"Summary of the changes made in filename1\",
|
||||
\"filename2 with path\": \"Summary of the changes made in filename2\",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Ensure that the summaries are accurate and reflect the changes made.
|
||||
- Ensure that the summary is concise and does not exceed 200 characters.
|
||||
- The response must be in JSON format, with filenames as keys and summaries as values.
|
||||
- Do not include any additional text or output outside of the JSON format.
|
||||
- The keys in the JSON object should correspond to real filenames present in the diff changes.
|
||||
|
||||
**User Input:**
|
||||
```
|
||||
{__USER_INPUT__}
|
||||
```
|
||||
|
||||
**Diff Changes:**
|
||||
```
|
||||
{__DIFF__}
|
||||
```
|
||||
---
|
||||
|
||||
Based on the provided diff changes and any additional user input, please generate a JSON object containing summaries for each modified file.
|
||||
"""
|
||||
prompt_summary_for_files_llm_config = {"model": "gpt-3.5-turbo-1106"}
|
||||
# ask summaries for missed files
|
||||
# missed files => {__MISSED_FILES__}
|
||||
PROMPT_SUMMARY_FOR_FILES_RETRY = """
|
||||
The following files are missed in your summary:
|
||||
{__MISSED_FILES__}
|
||||
"""
|
||||
|
||||
# group changes for files based diff
|
||||
# diff => {__DIFF__}
|
||||
PROMPT_GROUP_FILES = """
|
||||
Objective: **Categorize the modified files from a diff into groups based on their relevance to each other, and assign an importance level to each group. Limit the number of groups to a maximum of three.**
|
||||
|
||||
**Instructions:**
|
||||
1. **Analysis:** Review the diff content to discern related changes. Group files that are part of the same logical change, ensuring that the code will compile and run correctly post-commit.
|
||||
2. **Atomic Grouping:** Aim for the smallest possible groups. Each should represent a single, cohesive modification for clarity and independent comprehension. Do not exceed three groups in total.
|
||||
3. **Importance Level:** Rate each group's importance on a scale of 1 to 10, with 1 being the most critical. Consider the impact on functionality, urgency of fixes, and feature significance.
|
||||
|
||||
**Response Format:**
|
||||
- Use JSON format for your response.
|
||||
- Include all files from the diff content.
|
||||
- Structure the JSON as shown in the example below.
|
||||
|
||||
**Example Output:**
|
||||
```json
|
||||
{
|
||||
"groups": [
|
||||
{\"files\": [\"fileA\", \"fileB\"], \"group\": \"Feature Improvement\", \"importance_level\": 5},
|
||||
{\"files\": [\"fileC\"], \"group\": \"Bug Fix\", \"importance_level\": 1},
|
||||
{\"files\": [\"fileD\", \"fileE\"], \"group\": \"Code Refactoring\", \"importance_level\": 3}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Ensure the JSON output is valid and contains no additional text or characters.
|
||||
- Each group must be self-contained, with no cross-group dependencies.
|
||||
- The importance level should accurately reflect the priority for committing the changes.
|
||||
- The total number of groups must not exceed three.
|
||||
- Follows the JSON structure shown in the example above.
|
||||
|
||||
**Diff Content:**
|
||||
```
|
||||
{__DIFF__}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Based on the provided diff content, group the files accordingly and assign an appropriate importance level to each group, following the instructions and constraints.
|
||||
"""
|
||||
prompt_group_files_llm_config = {"model": "gpt-3.5-turbo-1106"}
|
||||
# re-group files based missed files
|
||||
# missed files => {__MISSED_FILES__}
|
||||
PROMPT_GROUP_FILES_RETRY = """
|
||||
The following files are missed in your response:
|
||||
{__MISSED_FILES__}
|
||||
Please re-group the files again, don't miss any file.
|
||||
"""
|
||||
|
||||
|
||||
# generate commit message based diff and user_input
|
||||
# diff => {__DIFF__}
|
||||
# user_input => {__USER_INPUT__}
|
||||
PROMPT_COMMIT_MESSAGE_BY_DIFF_USER_INPUT = """
|
||||
Objective:** Create a commit message that concisely summarizes the changes made to the codebase as reflected by the provided diff. The commit message should also take into account any additional context or instructions given by the user.
|
||||
|
||||
**Commit Message Structure:**
|
||||
1. **Title Line:** Start with a type from the following options: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, etc. Follow the type with a concise title. Format: `type: Title`. Only one title line is allowed.
|
||||
2. **Summary:** Provide a summary of all changes in no more than three detailed message lines. Each line should be prefixed with a \"-\".
|
||||
3. **Closing Reference (Optional):** If applicable, include a closing reference line in the format `Closes #IssueNumber`. Only include this if you know the exact issue number.
|
||||
|
||||
**Response Format:**
|
||||
```
|
||||
type: Title
|
||||
|
||||
Detail message line 1
|
||||
Detail message line 2
|
||||
Detail message line 3
|
||||
|
||||
Closes #IssueNumber
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Do not include markdown block flags (```) or the placeholder text \"commit_message\" in your response.
|
||||
- Adhere to best practices for commit messages:
|
||||
- Keep the title under 50 characters.
|
||||
- Keep each summary line under 72 characters.
|
||||
- If the exact issue number is unknown, omit the closing reference line.
|
||||
|
||||
**User Input:** `{__USER_INPUT__}`
|
||||
|
||||
**Code Changes:**
|
||||
```
|
||||
{__DIFF__}
|
||||
```
|
||||
---
|
||||
|
||||
Please use the above structure to generate a commit message that meets the specified criteria.
|
||||
"""
|
||||
prompt_commit_message_by_diff_user_input_llm_config = {"model": "gpt-3.5-turbo-1106"}
|
||||
|
||||
# generate commit message based file summary and user_input
|
||||
# file_summary => {__FILE_SUMMARY__}
|
||||
# user_input => {__USER_INPUT__}
|
||||
PROMPT_COMMIT_MESSAGE_BY_SUMMARY_USER_INPUT = """
|
||||
Objective:** Generate a commit message that accurately reflects the changes made to the codebase, as summarized by the AI-generated file summary and any additional user input.
|
||||
|
||||
**Commit Message Structure:**
|
||||
1. **Title Line:** Begin with a type from the following options: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, etc. The title should be concise and informative. Format: `type: Title`. Only one title line is allowed.
|
||||
2. **Summary:** Condense the changes into 1-3 detailed message lines, each beginning with a \"-\".
|
||||
3. **Closing Reference (Optional):** If known, include a closing reference in the format `Closes #IssueNumber`. If the exact issue number is unknown, omit this line.
|
||||
|
||||
**Response Format:**
|
||||
```
|
||||
type: Title
|
||||
|
||||
Detail message line 1
|
||||
Detail message line 2
|
||||
Detail message line 3
|
||||
|
||||
Closes #IssueNumber
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Exclude markdown code block flags (```) and the placeholder \"commit_message\" from your response.
|
||||
- Follow commit message best practices:
|
||||
- Title line should be under 50 characters.
|
||||
- Each summary line should be under 72 characters.
|
||||
- If the issue number is not provided, do not include the closing reference line.
|
||||
|
||||
**User Input:** `{__USER_INPUT__}`
|
||||
|
||||
**File Summary:**
|
||||
```
|
||||
{__FILE_SUMMARY__}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Please create a commit message following the above guidelines based on the provided file summary and user input.
|
||||
"""
|
||||
prompt_commit_message_by_summary_user_input_llm_config = {"model": "gpt-4-1106-preview"}
|
@ -1,5 +0,0 @@
|
||||
description: 为你选定的代码变更生成格式规范的提交信息,并通过 Git 提交。如需要可包含对应 issue 编号(例如,输入“/commit to close #12”)。
|
||||
hint: to close Issue #issue_number
|
||||
input: optional
|
||||
steps:
|
||||
- run: $devchat_python $command_path/../commit.py "$input" "chinese"
|
@ -1 +0,0 @@
|
||||
description: Generate a commit message for the given git diff.
|
@ -1,22 +0,0 @@
|
||||
As a software developer assistant, your task is to provide clear and concise responses and write commit messages based on given code, requirements, or conversations.
|
||||
Follow these guidelines:
|
||||
|
||||
1. A commit message should include a title and multiple body lines.
|
||||
2. Adhere to best practices. Keep the title under 50 characters and each body line under 72 characters.
|
||||
3. Format all commit messages by enclosing each message within a block of triple backticks (```), and include 'commitmsg' alongside the beginning backticks.
|
||||
For example:
|
||||
```commitmsg
|
||||
The title
|
||||
|
||||
- The first bullet point.
|
||||
- The second bullet point.
|
||||
```
|
||||
4. Utilize the diff output in the <context> to create the summary.
|
||||
5. Utilize the previous messages, if provided in the end of this prompt, to improve the title or bullet points by clearly conveying the intention of code changes.
|
||||
Note that not all previous messages are necessarily relevant.
|
||||
For example, disregard any previous commit messages you have written.
|
||||
You may encounter duplicate or conflicting messages, and the later messages should be considered as the most accurate.
|
||||
6. Prioritize the diff output in the given <context> and focus on actual code changes.
|
||||
Disregard any previous messages unrelated to the diff output.
|
||||
|
||||
If you need more information, ask for it.
|
3
community/README.md
Normal file
3
community/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Workflows
|
||||
|
||||
Workflows for DevChat contributed by the community.
|
25
community/aider/README.md
Normal file
25
community/aider/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
### aider 操作指南
|
||||
|
||||
aider是一个AI辅助的代码编辑工具,可以根据自然语言指令修改代码。
|
||||
|
||||
用途:
|
||||
根据用户提供的指令,自动分析和修改已添加到aider中的代码文件。
|
||||
|
||||
使用方法:
|
||||
1. 使用 `/aider.files.add` 命令添加需要处理的文件
|
||||
2. 输入 `/aider <message>` 命令,其中 `<message>` 是你想要aider执行的任务描述
|
||||
3. 等待aider生成建议的更改
|
||||
4. 在IDE中查看每个文件的Diff视图,选择是否接受修改
|
||||
5. 对于多个文件的更改,系统会在每个文件之后询问是否继续查看下一个文件的更改
|
||||
|
||||
注意事项:
|
||||
- 使用前必须先添加文件到aider,否则会提示使用 'aider.files.add' 命令
|
||||
- 可以使用 `aider.files.remove` 命令从aider中移除文件
|
||||
- 所有更改都会在IDE中以Diff视图形式展示,你可以决定是否应用这些更改
|
||||
- aider使用OpenAI的API,请确保已正确设置API密钥
|
||||
|
||||
示例:
|
||||
/aider 重构这段代码以提高性能
|
||||
|
||||
额外信息:
|
||||
aider支持多种编程语言,可以执行代码重构、bug修复、性能优化等任务。它会分析当前添加的所有文件,并提供整体的改进建议。
|
213
community/aider/command.py
Normal file
213
community/aider/command.py
Normal file
@ -0,0 +1,213 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from devchat.ide import IDEService
|
||||
|
||||
from lib.chatmark import Button
|
||||
|
||||
GLOBAL_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".chat", ".workflow_config.json")
|
||||
|
||||
|
||||
def save_config(config_path, item, value):
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
config[item] = value
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
|
||||
def write_python_path_to_config():
|
||||
"""
|
||||
Write the current system Python path to the configuration.
|
||||
"""
|
||||
python_path = sys.executable
|
||||
save_config(GLOBAL_CONFIG_PATH, "aider_python", python_path)
|
||||
print(f"Python path '{python_path}' has been written to the configuration.")
|
||||
|
||||
|
||||
def get_aider_files():
|
||||
"""
|
||||
从.chat/.aider_files文件中读取aider文件列表
|
||||
"""
|
||||
aider_files_path = os.path.join(".chat", ".aider_files")
|
||||
if not os.path.exists(aider_files_path):
|
||||
return []
|
||||
|
||||
with open(aider_files_path, "r") as f:
|
||||
return [line.strip() for line in f if line.strip()]
|
||||
|
||||
|
||||
def run_aider(message, files):
|
||||
"""
|
||||
运行aider命令
|
||||
"""
|
||||
python = sys.executable
|
||||
model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
|
||||
|
||||
cmd = [
|
||||
python,
|
||||
"-m",
|
||||
"aider",
|
||||
"--model",
|
||||
f"openai/{model}",
|
||||
"--yes",
|
||||
"--no-auto-commits",
|
||||
"--dry-run",
|
||||
"--no-pretty",
|
||||
"--message",
|
||||
message,
|
||||
] + files
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
|
||||
has_started = False
|
||||
aider_output = ""
|
||||
for line in process.stdout:
|
||||
if "run with --help" in line or 'run "aider --help"' in line:
|
||||
has_started = True
|
||||
continue
|
||||
if has_started:
|
||||
aider_output += line
|
||||
print(line, end="", flush=True)
|
||||
|
||||
return_code = process.wait()
|
||||
|
||||
if return_code != 0:
|
||||
for line in process.stderr:
|
||||
print(f"Error: {line.strip()}", file=sys.stderr)
|
||||
sys.exit(return_code)
|
||||
|
||||
return aider_output
|
||||
|
||||
|
||||
def apply_changes(changes, files):
|
||||
"""
|
||||
应用aider生成的更改
|
||||
"""
|
||||
changes_file = ".chat/changes.txt"
|
||||
os.makedirs(os.path.dirname(changes_file), exist_ok=True)
|
||||
with open(changes_file, "w") as f:
|
||||
f.write(changes)
|
||||
|
||||
python = sys.executable
|
||||
model = os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
|
||||
|
||||
cmd = [
|
||||
python,
|
||||
"-m",
|
||||
"aider",
|
||||
"--model",
|
||||
f"openai/{model}",
|
||||
"--yes",
|
||||
"--no-auto-commits",
|
||||
"--apply",
|
||||
changes_file,
|
||||
] + files
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
|
||||
has_started = False
|
||||
for line in process.stdout:
|
||||
if "Model:" in line:
|
||||
has_started = True
|
||||
continue
|
||||
if has_started:
|
||||
print(line, end="", flush=True)
|
||||
|
||||
return_code = process.wait()
|
||||
|
||||
if return_code != 0:
|
||||
for line in process.stderr:
|
||||
print(f"Error: {line.strip()}", file=sys.stderr)
|
||||
sys.exit(return_code)
|
||||
|
||||
os.remove(changes_file)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function to run the aider command.
|
||||
|
||||
This function performs the following tasks:
|
||||
1. Checks for correct command-line usage
|
||||
2. Writes the current Python path to the configuration
|
||||
3. Retrieves the list of files to be processed
|
||||
4. Runs the aider command with the given message
|
||||
5. Applies the suggested changes
|
||||
6. Displays the differences in the IDE
|
||||
|
||||
Usage: python command.py <message>
|
||||
"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python command.py <message>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
write_python_path_to_config()
|
||||
|
||||
message = sys.argv[1]
|
||||
files = get_aider_files()
|
||||
|
||||
if not files:
|
||||
print(
|
||||
"No files added to aider. Please add files using 'aider.files.add' command.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print("Running aider...\n", flush=True)
|
||||
changes = run_aider(message, files)
|
||||
|
||||
if not changes:
|
||||
print("No changes suggested by aider.")
|
||||
sys.exit(0)
|
||||
|
||||
print("\nApplying changes...\n", flush=True)
|
||||
|
||||
# 保存原始文件内容
|
||||
original_contents = {}
|
||||
for file in files:
|
||||
with open(file, "r") as f:
|
||||
original_contents[file] = f.read()
|
||||
|
||||
# 应用更改
|
||||
apply_changes(changes, files)
|
||||
|
||||
# 读取更新后的文件内容
|
||||
updated_contents = {}
|
||||
for file in files:
|
||||
with open(file, "r") as f:
|
||||
updated_contents[file] = f.read()
|
||||
|
||||
# 还原原始文件内容
|
||||
for file in files:
|
||||
with open(file, "w") as f:
|
||||
f.write(original_contents[file])
|
||||
|
||||
# 使用 IDEService 展示差异
|
||||
ide_service = IDEService()
|
||||
for index, file in enumerate(files):
|
||||
ide_service.diff_apply(file, updated_contents[file])
|
||||
if index < len(files) - 1:
|
||||
# 等待用户确认
|
||||
button = Button(
|
||||
["Show Next Changes", "Cancel"],
|
||||
)
|
||||
button.render()
|
||||
|
||||
idx = button.clicked
|
||||
if idx == 0:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
print("Changes have been displayed in the IDE.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
9
community/aider/command.yml.bak
Normal file
9
community/aider/command.yml.bak
Normal file
@ -0,0 +1,9 @@
|
||||
description: "aider command"
|
||||
workflow_python:
|
||||
env_name: devchat-aider-env
|
||||
version: 3.11.0
|
||||
dependencies: requirements.txt
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $workflow_python $command_path/command.py "$input"
|
23
community/aider/files/add/README.md
Normal file
23
community/aider/files/add/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
### aider.files.add
|
||||
|
||||
这个命令用于将文件添加到aider的处理列表中。
|
||||
|
||||
用途:
|
||||
添加指定文件到aider,使其包含在后续的aider操作中。
|
||||
|
||||
使用方法:
|
||||
/aider.files.add <file_path>
|
||||
|
||||
参数:
|
||||
- <file_path>: 要添加的文件路径(必需)
|
||||
|
||||
注意事项:
|
||||
- 文件路径必须是有效的格式
|
||||
- 已存在于列表中的文件不会重复添加
|
||||
- 成功添加后会显示当前的aider文件列表
|
||||
|
||||
示例:
|
||||
/aider.files.add src/main.py
|
||||
|
||||
额外信息:
|
||||
这个命令会将文件路径保存到.chat/.aider_files文件中。如果.chat目录不存在,会自动创建。
|
66
community/aider/files/add/command.py
Normal file
66
community/aider/files/add/command.py
Normal file
@ -0,0 +1,66 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def is_valid_path(path):
|
||||
"""
|
||||
检查路径是否为有效的文件路径形式
|
||||
"""
|
||||
try:
|
||||
# 尝试规范化路径
|
||||
normalized_path = os.path.normpath(path)
|
||||
# 检查路径是否是绝对路径或相对路径
|
||||
return (
|
||||
os.path.isabs(normalized_path)
|
||||
or not os.path.dirname(normalized_path) == normalized_path
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def add_file(file_path):
|
||||
# 1. 检查是否为有效的文件路径形式
|
||||
if not is_valid_path(file_path):
|
||||
print(f"Error: '{file_path}' is not a valid file path format.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 获取绝对路径
|
||||
abs_file_path = file_path.strip()
|
||||
|
||||
# 2. 将新增文件路径存储到.chat/.aider_files文件中
|
||||
aider_files_path = os.path.join(".chat", ".aider_files")
|
||||
|
||||
# 确保.chat目录存在
|
||||
os.makedirs(os.path.dirname(aider_files_path), exist_ok=True)
|
||||
|
||||
# 读取现有文件列表
|
||||
existing_files = set()
|
||||
if os.path.exists(aider_files_path):
|
||||
with open(aider_files_path, "r") as f:
|
||||
existing_files = set(line.strip() for line in f)
|
||||
|
||||
# 添加新文件
|
||||
existing_files.add(abs_file_path)
|
||||
|
||||
# 写入更新后的文件列表
|
||||
with open(aider_files_path, "w") as f:
|
||||
for file in sorted(existing_files):
|
||||
f.write(f"{file}\n")
|
||||
|
||||
print(f"Added '{abs_file_path}' to aider files.")
|
||||
print("\nCurrent aider files:")
|
||||
for file in sorted(existing_files):
|
||||
print(f"- {file}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2 or sys.argv[1].strip() == "":
|
||||
print("Usage: /aider.files.add <file_path>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
add_file(file_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/aider/files/add/command.yml
Normal file
5
community/aider/files/add/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: "add files to aider"
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
19
community/aider/files/list/README.md
Normal file
19
community/aider/files/list/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
### aider.files.list
|
||||
|
||||
这个命令用于列出当前在aider处理列表中的所有文件。
|
||||
|
||||
用途:
|
||||
显示所有已添加到aider中的文件,提供当前aider正在处理的文件概览。
|
||||
|
||||
使用方法:
|
||||
/aider.files.list
|
||||
|
||||
注意事项:
|
||||
- 如果没有文件被添加到aider,会显示相应的提示消息
|
||||
- 文件列表按字母顺序排序显示
|
||||
|
||||
示例:
|
||||
/aider.files.list
|
||||
|
||||
额外信息:
|
||||
这个命令会读取.chat/.aider_files文件的内容来获取文件列表。如果该文件不存在,会提示尚未添加任何文件。
|
31
community/aider/files/list/command.py
Normal file
31
community/aider/files/list/command.py
Normal file
@ -0,0 +1,31 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def list_files():
|
||||
aider_files_path = os.path.join(".chat", ".aider_files")
|
||||
|
||||
# 确保.chat/.aider_files文件存在
|
||||
if not os.path.exists(aider_files_path):
|
||||
print("No files have been added to aider yet.")
|
||||
sys.exit(0)
|
||||
|
||||
# 读取文件列表
|
||||
with open(aider_files_path, "r") as f:
|
||||
files = [line.strip() for line in f]
|
||||
|
||||
# 打印文件列表
|
||||
if files:
|
||||
print("Aider files:")
|
||||
for file in sorted(files):
|
||||
print(f"- {file}")
|
||||
else:
|
||||
print("No files found in aider.")
|
||||
|
||||
|
||||
def main():
|
||||
list_files()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
4
community/aider/files/list/command.yml
Normal file
4
community/aider/files/list/command.yml
Normal file
@ -0,0 +1,4 @@
|
||||
description: "list files in aider"
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py
|
23
community/aider/files/remove/README.md
Normal file
23
community/aider/files/remove/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
### aider.files.remove
|
||||
|
||||
这个命令用于从aider处理列表中移除指定的文件。
|
||||
|
||||
用途:
|
||||
将指定文件从aider的处理列表中删除,使其不再包含在后续的aider操作中。
|
||||
|
||||
使用方法:
|
||||
/aider.files.remove <file_path>
|
||||
|
||||
参数:
|
||||
- <file_path>: 要移除的文件路径(必需)
|
||||
|
||||
注意事项:
|
||||
- 文件路径必须是有效的格式
|
||||
- 如果指定的文件不在列表中,会显示相应的提示消息
|
||||
- 成功移除后会显示更新后的aider文件列表
|
||||
|
||||
示例:
|
||||
/aider.files.remove src/main.py
|
||||
|
||||
额外信息:
|
||||
这个命令会更新.chat/.aider_files文件,从中删除指定的文件路径。如果文件不存在于列表中,操作会安全退出。
|
72
community/aider/files/remove/command.py
Normal file
72
community/aider/files/remove/command.py
Normal file
@ -0,0 +1,72 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def is_valid_path(path):
|
||||
"""
|
||||
检查路径是否为有效的文件路径形式
|
||||
"""
|
||||
try:
|
||||
# 尝试规范化路径
|
||||
normalized_path = os.path.normpath(path)
|
||||
# 检查路径是否是绝对路径或相对路径
|
||||
return (
|
||||
os.path.isabs(normalized_path)
|
||||
or not os.path.dirname(normalized_path) == normalized_path
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def remove_file(file_path):
|
||||
# 1. 检查是否为有效的文件路径形式
|
||||
if not is_valid_path(file_path):
|
||||
print(f"Error: '{file_path}' is not a valid file path format.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 获取绝对路径
|
||||
abs_file_path = file_path.strip()
|
||||
|
||||
# 2. 从.chat/.aider_files文件中移除指定文件路径
|
||||
aider_files_path = os.path.join(".chat", ".aider_files")
|
||||
|
||||
# 确保.chat目录存在
|
||||
if not os.path.exists(aider_files_path):
|
||||
print(f"Error: '{aider_files_path}' does not exist.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 读取现有文件列表
|
||||
existing_files = set()
|
||||
with open(aider_files_path, "r") as f:
|
||||
existing_files = set(line.strip() for line in f)
|
||||
|
||||
# 检查文件是否在列表中
|
||||
if abs_file_path not in existing_files:
|
||||
print(f"'{abs_file_path}' is not in aider files.")
|
||||
sys.exit(0)
|
||||
|
||||
# 移除文件
|
||||
existing_files.remove(abs_file_path)
|
||||
|
||||
# 写入更新后的文件列表
|
||||
with open(aider_files_path, "w") as f:
|
||||
for file in sorted(existing_files):
|
||||
f.write(f"{file}\n")
|
||||
|
||||
print(f"Removed '{abs_file_path}' from aider files.")
|
||||
print("\nCurrent aider files:")
|
||||
for file in sorted(existing_files):
|
||||
print(f"- {file}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2 or sys.argv[1].strip() == "":
|
||||
print("Usage: /aider.files.remove <file_path>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
remove_file(file_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/aider/files/remove/command.yml
Normal file
5
community/aider/files/remove/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: "remove files from aider"
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
2
community/aider/requirements.txt
Normal file
2
community/aider/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
git+https://gitee.com/imlaji/aider.git@main
|
||||
git+https://gitee.com/devchat-ai/devchat.git@aider
|
24
community/github/code_task_summary/README.md
Normal file
24
community/github/code_task_summary/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
### code_task_summary
|
||||
|
||||
根据当前分支或指定的Issue,生成代码任务摘要。
|
||||
|
||||
#### 用途
|
||||
- 自动生成简洁的代码任务描述
|
||||
- 帮助开发者快速理解任务要点
|
||||
- 用于更新项目配置或文档
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.code_task_summary [issue_url]`
|
||||
|
||||
- 如不提供issue_url,将基于当前分支名称提取Issue信息
|
||||
- 如提供issue_url,将直接使用该Issue的内容
|
||||
|
||||
#### 操作流程
|
||||
1. 获取Issue信息
|
||||
2. 生成代码任务摘要
|
||||
3. 允许用户编辑摘要
|
||||
4. 更新项目配置文件
|
||||
|
||||
#### 注意事项
|
||||
- 确保Git仓库配置正确
|
||||
- 需要有效的GitHub Token以访问API
|
120
community/github/code_task_summary/command.py
Normal file
120
community/github/code_task_summary/command.py
Normal file
@ -0,0 +1,120 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_json
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
check_git_installed,
|
||||
get_current_branch,
|
||||
get_github_repo,
|
||||
get_issue_info,
|
||||
is_issue_url,
|
||||
read_issue_by_url,
|
||||
)
|
||||
|
||||
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
# Function to generate a random branch name
|
||||
PROMPT = (
|
||||
"You are a coding engineer, required to summarize the ISSUE description into a coding task description of no more than 50 words. \n" # noqa: E501
|
||||
"The ISSUE description is as follows: {issue_body}, please summarize the corresponding coding task description.\n" # noqa: E501
|
||||
'The coding task description should be output in JSON format, in the form of: {{"summary": "code task summary"}}\n' # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_code_task_summary(issue_body):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit code task summary:")
|
||||
def edit_code_task_summary(task_summary):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_or_task(task):
|
||||
if is_issue_url(task):
|
||||
issue = read_issue_by_url(task.strip())
|
||||
assert_exit(not issue, "Failed to read issue.", exit_code=-1)
|
||||
|
||||
return json.dumps({"id": issue["number"], "title": issue["title"], "body": issue["body"]})
|
||||
else:
|
||||
return task
|
||||
|
||||
|
||||
def get_issue_json(issue_id, task):
|
||||
issue = {"id": "no issue id", "title": "", "body": task}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, "Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"html_url": issue["html_url"],
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("Start update code task summary ...", end="\n\n", flush=True)
|
||||
|
||||
is_git_installed = check_git_installed()
|
||||
assert_exit(not is_git_installed, "Git is not installed.", exit_code=-1)
|
||||
|
||||
task = sys.argv[1]
|
||||
|
||||
repo_name = get_github_repo()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
|
||||
# print basic info, repo_name, branch_name, issue_id
|
||||
print("repo name:", repo_name, end="\n\n")
|
||||
print("branch name:", branch_name, end="\n\n")
|
||||
print("issue id:", issue_id, end="\n\n")
|
||||
|
||||
issue = get_issue_json(issue_id, task)
|
||||
assert_exit(not issue["body"], "Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
|
||||
# Generate 5 branch names
|
||||
print("Generating code task summary ...", end="\n\n", flush=True)
|
||||
code_task_summary = generate_code_task_summary(issue_body=issue["body"])
|
||||
assert_exit(not code_task_summary, "Failed to generate code task summary.", exit_code=-1)
|
||||
assert_exit(
|
||||
not code_task_summary.get("summary", None),
|
||||
"Failed to generate code task summary, missing summary field in result.",
|
||||
exit_code=-1,
|
||||
)
|
||||
code_task_summary = code_task_summary["summary"]
|
||||
|
||||
# Select branch name
|
||||
code_task_summary = edit_code_task_summary(code_task_summary)
|
||||
assert_exit(not code_task_summary, "Failed to edit code task summary.", exit_code=-1)
|
||||
code_task_summary = code_task_summary[0]
|
||||
|
||||
# create and checkout branch
|
||||
print("Updating code task summary to config:")
|
||||
config_file = os.path.join(".chat", "complete.config")
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, "r") as f:
|
||||
config = json.load(f)
|
||||
config["taskDescription"] = code_task_summary
|
||||
else:
|
||||
config = {"taskDescription": code_task_summary}
|
||||
with open(config_file, "w") as f:
|
||||
json.dump(config, f, indent=4)
|
||||
print("Code task summary has updated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/code_task_summary/command.yml
Normal file
5
community/github/code_task_summary/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Generate code task summary.'
|
||||
input: optional
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
1
community/github/command.yml
Normal file
1
community/github/command.yml
Normal file
@ -0,0 +1 @@
|
||||
description: Root of github commands.
|
23
community/github/commit/README.md
Normal file
23
community/github/commit/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
### commit
|
||||
|
||||
自动生成提交信息并执行Git提交。
|
||||
|
||||
#### 用途
|
||||
- 生成规范的提交信息
|
||||
- 简化Git提交流程
|
||||
- 保持提交历史的一致性
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.commit [message]`
|
||||
|
||||
- message: 可选的用户输入,用于辅助生成提交信息
|
||||
|
||||
#### 操作流程
|
||||
1. 选择要提交的文件
|
||||
2. 生成提交信息
|
||||
3. 允许用户编辑提交信息
|
||||
4. 执行Git提交
|
||||
|
||||
#### 注意事项
|
||||
- 确保已选择需要提交的文件
|
||||
- 生成的提交信息可能需要进一步修改以符合项目规范
|
6
community/github/commit/command.yml
Normal file
6
community/github/commit/command.yml
Normal file
@ -0,0 +1,6 @@
|
||||
description: 'Writes a well-formatted commit message for selected code changes and commits them via Git. Include an issue number if desired (e.g., input "/commit to close #12").'
|
||||
hint: to close Issue #issue_number
|
||||
input: optional
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/commit.py "$input" "english"
|
539
community/github/commit/commit.py
Normal file
539
community/github/commit/commit.py
Normal file
@ -0,0 +1,539 @@
|
||||
# flake8: noqa: E402
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_completion_stream
|
||||
|
||||
from lib.chatmark import Button, Checkbox, Form, TextEditor
|
||||
from lib.ide_service import IDEService
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit # noqa: E402
|
||||
from git_api import (
|
||||
get_github_repo,
|
||||
get_github_repo_issues,
|
||||
get_github_username,
|
||||
get_issue_info,
|
||||
subprocess_check_output,
|
||||
subprocess_run,
|
||||
)
|
||||
|
||||
diff_too_large_message_en = (
|
||||
"Commit failed. The modified content is too long "
|
||||
"and exceeds the model's length limit. "
|
||||
"You can try to make partial changes to the file and submit multiple times. "
|
||||
"Making small changes and submitting them multiple times is a better practice."
|
||||
)
|
||||
diff_too_large_message_zh = (
|
||||
"提交失败。修改内容太长,超出模型限制长度,"
|
||||
"可以尝试选择部分修改文件多次提交,小修改多提交是更好的做法。"
|
||||
)
|
||||
|
||||
COMMIT_PROMPT_LIMIT_SIZE = 20000
|
||||
|
||||
|
||||
def extract_markdown_block(text):
|
||||
"""
|
||||
Extracts the first Markdown code block from the given text without the language specifier.
|
||||
|
||||
:param text: A string containing Markdown text
|
||||
:return: The content of the first Markdown code block, or None if not found
|
||||
"""
|
||||
# 正则表达式匹配Markdown代码块,忽略可选的语言类型标记
|
||||
pattern = r"```(?:\w+)?\s*\n(.*?)\n```"
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
|
||||
if match:
|
||||
# 返回第一个匹配的代码块内容,去除首尾的反引号和语言类型标记
|
||||
# 去除块结束标记前的一个换行符,但保留其他内容
|
||||
block_content = match.group(1)
|
||||
return block_content
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
# Read the prompt from the diffCommitMessagePrompt.txt file
|
||||
def read_prompt_from_file(filename):
|
||||
"""
|
||||
Reads the content of a file and returns it as a string.
|
||||
|
||||
This function is designed to read a prompt message from a text file.
|
||||
It expects the file to be encoded in UTF-8 and will strip any leading
|
||||
or trailing whitespace from the content of the file. If the file does
|
||||
not exist or an error occurs during reading, the function logs an error
|
||||
message and exits the script.
|
||||
|
||||
Parameters:
|
||||
- filename (str): The path to the file that contains the prompt message.
|
||||
|
||||
Returns:
|
||||
- str: The content of the file as a string.
|
||||
|
||||
Raises:
|
||||
- FileNotFoundError: If the file does not exist.
|
||||
- Exception: If any other error occurs during file reading.
|
||||
"""
|
||||
try:
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
return file.read().strip()
|
||||
except FileNotFoundError:
|
||||
IDEService().ide_logging(
|
||||
"error",
|
||||
f"File {filename} not found. "
|
||||
"Please make sure it exists in the same directory as the script.",
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
IDEService().ide_logging(
|
||||
"error", f"An error occurred while reading the file {filename}: {e}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Read the prompt content from the file
|
||||
script_path = os.path.dirname(__file__)
|
||||
PROMPT_FILENAME = os.path.join(script_path, "diffCommitMessagePrompt.txt")
|
||||
PROMPT_COMMIT_MESSAGE_BY_DIFF_USER_INPUT = read_prompt_from_file(PROMPT_FILENAME)
|
||||
prompt_commit_message_by_diff_user_input_llm_config = {
|
||||
"model": os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
|
||||
}
|
||||
|
||||
|
||||
language = ""
|
||||
|
||||
|
||||
def assert_value(value, message):
|
||||
"""
|
||||
判断给定的value是否为True,如果是,则输出指定的message并终止程序。
|
||||
|
||||
Args:
|
||||
value: 用于判断的值。
|
||||
message: 如果value为True时需要输出的信息。
|
||||
|
||||
Returns:
|
||||
无返回值。
|
||||
|
||||
"""
|
||||
if value:
|
||||
print(message, file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def decode_path(encoded_path):
|
||||
octal_pattern = re.compile(r"\\[0-7]{3}")
|
||||
|
||||
if octal_pattern.search(encoded_path):
|
||||
bytes_path = encoded_path.encode("utf-8").decode("unicode_escape").encode("latin1")
|
||||
decoded_path = bytes_path.decode("utf-8")
|
||||
return decoded_path
|
||||
else:
|
||||
return encoded_path
|
||||
|
||||
|
||||
def get_modified_files():
|
||||
"""
|
||||
获取当前修改文件列表以及已经staged的文件列表
|
||||
|
||||
Args:
|
||||
无
|
||||
|
||||
Returns:
|
||||
tuple: 包含两个list的元组,第一个list包含当前修改过的文件,第二个list包含已经staged的文件
|
||||
"""
|
||||
""" 获取当前修改文件列表以及已经staged的文件列表"""
|
||||
output = subprocess_check_output(["git", "status", "-s", "-u"], text=True, encoding="utf-8")
|
||||
lines = output.split("\n")
|
||||
modified_files = []
|
||||
staged_files = []
|
||||
|
||||
def strip_file_name(file_name):
|
||||
file = file_name.strip()
|
||||
if file.startswith('"'):
|
||||
file = file[1:-1]
|
||||
return file
|
||||
|
||||
for line in lines:
|
||||
if len(line) > 2:
|
||||
status, filename = line[:2], decode_path(line[3:])
|
||||
# check wether filename is a directory
|
||||
if os.path.isdir(filename):
|
||||
continue
|
||||
modified_files.append(os.path.normpath(strip_file_name(filename)))
|
||||
if status == "M " or status == "A " or status == "D ":
|
||||
staged_files.append(os.path.normpath(strip_file_name(filename)))
|
||||
return modified_files, staged_files
|
||||
|
||||
|
||||
def get_marked_files(modified_files, staged_files):
|
||||
"""
|
||||
根据给定的参数获取用户选中以供提交的文件
|
||||
|
||||
Args:
|
||||
modified_files (List[str]): 用户已修改文件列表
|
||||
staged_files (List[str]): 用户已staged文件列表
|
||||
|
||||
Returns:
|
||||
List[str]: 用户选中的文件列表
|
||||
"""
|
||||
# Create two Checkbox instances for staged and unstaged files
|
||||
staged_checkbox = Checkbox(staged_files, [True] * len(staged_files))
|
||||
|
||||
unstaged_files = [file for file in modified_files if file not in staged_files]
|
||||
unstaged_checkbox = Checkbox(unstaged_files, [False] * len(unstaged_files))
|
||||
|
||||
# Create a Form with both Checkbox instances
|
||||
form_list = []
|
||||
if len(staged_files) > 0:
|
||||
form_list.append("Staged:\n\n")
|
||||
form_list.append(staged_checkbox)
|
||||
|
||||
if len(unstaged_files) > 0:
|
||||
form_list.append("Unstaged:\n\n")
|
||||
form_list.append(unstaged_checkbox)
|
||||
|
||||
form = Form(form_list, submit_button_name="Continue")
|
||||
|
||||
# Render the Form and get user input
|
||||
form.render()
|
||||
|
||||
# Retrieve the selected files from both Checkbox instances
|
||||
staged_checkbox_selections = staged_checkbox.selections if staged_checkbox.selections else []
|
||||
unstaged_selections = unstaged_checkbox.selections if unstaged_checkbox.selections else []
|
||||
selected_staged_files = [staged_files[idx] for idx in staged_checkbox_selections]
|
||||
selected_unstaged_files = [unstaged_files[idx] for idx in unstaged_selections]
|
||||
|
||||
# Combine the selections from both checkboxes
|
||||
selected_files = selected_staged_files + selected_unstaged_files
|
||||
|
||||
return selected_files
|
||||
|
||||
|
||||
def rebuild_stage_list(user_files):
|
||||
"""
|
||||
根据用户选中文件,重新构建stage列表
|
||||
|
||||
Args:
|
||||
user_files: 用户选中的文件列表
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# Unstage all files
|
||||
subprocess_check_output(["git", "reset"])
|
||||
# Stage all user_files
|
||||
for file in user_files:
|
||||
subprocess_run(["git", "add", file])
|
||||
|
||||
|
||||
def get_diff():
|
||||
"""
|
||||
获取暂存区文件的Diff信息
|
||||
|
||||
Args:
|
||||
无
|
||||
|
||||
Returns:
|
||||
bytes: 返回bytes类型,是git diff --cached命令的输出结果
|
||||
|
||||
"""
|
||||
return subprocess_check_output(["git", "diff", "--cached"])
|
||||
|
||||
|
||||
def get_current_branch():
|
||||
try:
|
||||
# 使用git命令获取当前分支名称
|
||||
result = subprocess_check_output(
|
||||
["git", "branch", "--show-current"], stderr=subprocess.STDOUT
|
||||
).strip()
|
||||
# 将结果从bytes转换为str
|
||||
current_branch = result.decode("utf-8")
|
||||
return current_branch
|
||||
except subprocess.CalledProcessError:
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
return None
|
||||
|
||||
|
||||
def generate_commit_message_base_diff(user_input, diff, issue):
|
||||
"""
|
||||
根据diff信息,通过AI生成一个commit消息
|
||||
|
||||
Args:
|
||||
user_input (str): 用户输入的commit信息
|
||||
diff (str): 提交的diff信息
|
||||
|
||||
Returns:
|
||||
str: 生成的commit消息
|
||||
|
||||
"""
|
||||
global language
|
||||
language_prompt = "You must response commit message in chinese。\n" if language == "zh" else ""
|
||||
prompt = (
|
||||
PROMPT_COMMIT_MESSAGE_BY_DIFF_USER_INPUT.replace("{__DIFF__}", f"{diff}")
|
||||
.replace("{__USER_INPUT__}", f"{user_input + language_prompt}")
|
||||
.replace("{__ISSUE__}", f"{issue}")
|
||||
)
|
||||
|
||||
model_token_limit_error = (
|
||||
diff_too_large_message_en if language == "en" else diff_too_large_message_zh
|
||||
)
|
||||
if len(str(prompt)) > COMMIT_PROMPT_LIMIT_SIZE:
|
||||
print(model_token_limit_error, flush=True)
|
||||
sys.exit(0)
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
response = chat_completion_stream(messages, prompt_commit_message_by_diff_user_input_llm_config)
|
||||
|
||||
if (
|
||||
not response["content"]
|
||||
and response.get("error", None)
|
||||
and f"{response['error']}".find("This model's maximum context length is") > 0
|
||||
):
|
||||
print(model_token_limit_error)
|
||||
sys.exit(0)
|
||||
|
||||
assert_value(not response["content"], response.get("error", ""))
|
||||
response["content"] = extract_markdown_block(response["content"])
|
||||
return response
|
||||
|
||||
|
||||
def display_commit_message_and_commit(commit_message):
|
||||
"""
|
||||
展示提交信息并提交。
|
||||
|
||||
Args:
|
||||
commit_message: 提交信息。
|
||||
|
||||
Returns:
|
||||
None。
|
||||
|
||||
"""
|
||||
text_editor = TextEditor(commit_message, submit_button_name="Commit")
|
||||
text_editor.render()
|
||||
|
||||
new_commit_message = text_editor.new_text
|
||||
if not new_commit_message:
|
||||
return None
|
||||
return subprocess_check_output(["git", "commit", "-m", new_commit_message])
|
||||
|
||||
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
def get_issue_json(issue_id):
|
||||
issue = {"id": "no issue id", "title": "", "body": ""}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, "Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"html_url": issue["html_url"],
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
def check_git_installed():
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "--version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
except FileNotFoundError:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
except Exception:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def ask_for_push():
|
||||
"""
|
||||
询问用户是否要推送(push)更改到远程仓库
|
||||
|
||||
Returns:
|
||||
bool: 用户是否选择推送
|
||||
"""
|
||||
|
||||
print(
|
||||
"Step 3/3: Would you like to push your commit to the remote repository?",
|
||||
end="\n\n",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
button = Button(["Yes, push now", "No, I'll push later"])
|
||||
button.render()
|
||||
|
||||
return button.clicked == 0 # 如果用户点击第一个按钮(Yes),则返回True
|
||||
|
||||
|
||||
def push_changes():
|
||||
"""
|
||||
推送更改到远程仓库
|
||||
|
||||
Returns:
|
||||
bool: 推送是否成功
|
||||
"""
|
||||
try:
|
||||
current_branch = get_current_branch()
|
||||
if not current_branch:
|
||||
print(
|
||||
"Could not determine current branch. Push failed.",
|
||||
end="\n\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return False
|
||||
|
||||
print(f"Pushing changes to origin/{current_branch}...", end="\n\n", flush=True)
|
||||
result = subprocess_run(
|
||||
["git", "push", "origin", current_branch],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Push failed: {result.stderr}", end="\n\n", flush=True)
|
||||
return False
|
||||
print("Push completed successfully.", end="\n\n", flush=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Push failed: {str(e)}", end="\n\n", file=sys.stderr, flush=True)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(
|
||||
f"An unexpected error occurred: {str(e)}",
|
||||
end="\n\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def get_selected_issue_ids():
|
||||
"""
|
||||
获取用户选中的issue id
|
||||
|
||||
Returns:
|
||||
list: 用户选中的issue id列表
|
||||
"""
|
||||
name = get_github_username()
|
||||
issue_repo = get_github_repo(True)
|
||||
issues = get_github_repo_issues(issue_repo, assignee=name, state="open")
|
||||
if issues:
|
||||
checkbox = Checkbox(
|
||||
[f"#{issue['number']}: {issue['title']}" for issue in issues],
|
||||
title="Select the issues you want to close",
|
||||
)
|
||||
checkbox.render()
|
||||
return [issues[idx]["number"] for idx in checkbox.selections]
|
||||
|
||||
|
||||
def main():
|
||||
global language
|
||||
try:
|
||||
print("Let's follow the steps below.\n\n")
|
||||
# Ensure enough command line arguments are provided
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
"Usage: python script.py <user_input> <language>",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
sys.exit(-1)
|
||||
|
||||
user_input = sys.argv[1]
|
||||
language = "english"
|
||||
if len(sys.argv) > 2:
|
||||
language = sys.argv[2]
|
||||
|
||||
if not check_git_installed():
|
||||
sys.exit(-1)
|
||||
|
||||
print(
|
||||
"Step 1/3: Select the files you've changed that you wish to include in this commit, "
|
||||
"then click 'Submit'.",
|
||||
end="\n\n",
|
||||
flush=True,
|
||||
)
|
||||
modified_files, staged_files = get_modified_files()
|
||||
if len(modified_files) == 0:
|
||||
print("No files to commit.", file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
selected_files = get_marked_files(modified_files, staged_files)
|
||||
if not selected_files:
|
||||
print("No files selected, commit aborted.")
|
||||
return
|
||||
|
||||
rebuild_stage_list(selected_files)
|
||||
|
||||
print(
|
||||
"Step 2/3: Review the commit message I've drafted for you. "
|
||||
"Edit it below if needed. Then click 'Commit' to proceed with "
|
||||
"the commit using this message.",
|
||||
end="\n\n",
|
||||
flush=True,
|
||||
)
|
||||
diff = get_diff()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
issue = str(get_issue_json(issue_id))
|
||||
if branch_name:
|
||||
user_input += "\ncurrent repo branch name is:" + branch_name
|
||||
commit_message = generate_commit_message_base_diff(user_input, diff, issue)
|
||||
|
||||
# TODO
|
||||
# remove Closes #IssueNumber in commit message
|
||||
commit_message["content"] = (
|
||||
commit_message["content"]
|
||||
.replace("Closes #IssueNumber", "")
|
||||
.replace("No specific issue to close", "")
|
||||
.replace("No specific issue mentioned.", "")
|
||||
)
|
||||
# add closes #IssueNumber in commit message from issues from user selected
|
||||
issue_ids = get_selected_issue_ids()
|
||||
if issue_ids:
|
||||
issue_repo = get_github_repo(True)
|
||||
owner_repo = get_github_repo()
|
||||
closes_issue_contents = []
|
||||
for issue_id in issue_ids:
|
||||
closes_issue_contents.append(
|
||||
f"#{issue_id}" if owner_repo == issue_repo else f"{issue_repo}#{issue_id}"
|
||||
)
|
||||
commit_message["content"] += f"\n\nCloses {', '.join(closes_issue_contents)}"
|
||||
commit_result = display_commit_message_and_commit(commit_message["content"])
|
||||
if not commit_result:
|
||||
print("Commit aborted.", flush=True)
|
||||
else:
|
||||
# 添加推送步骤
|
||||
if ask_for_push():
|
||||
if not push_changes():
|
||||
print("Push failed.", flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
print("Commit completed.", flush=True)
|
||||
sys.exit(0)
|
||||
except Exception as err:
|
||||
print("Exception:", err, file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
45
community/github/commit/diffCommitMessagePrompt.txt
Normal file
45
community/github/commit/diffCommitMessagePrompt.txt
Normal file
@ -0,0 +1,45 @@
|
||||
Objective:** Generate a commit message that succinctly describes the codebase changes reflected in the provided diff, while incorporating any extra context or guidance from the user.
|
||||
|
||||
**Commit Message Structure:**
|
||||
1. **Title Line:** Choose a type such as `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, and so on, and couple it with a succinct title. Use the format: `type: Title`. Only one title line is permissible.
|
||||
2. **Summary:** Summarize all adjustments concisely within a maximum of three detailed message lines. Prefix each line with a \"-\".
|
||||
3. **Closing Reference (Conditional):** Include the line `Closes #IssueNumber` only if a specific, relevant issue number has been mentioned in the user input.
|
||||
|
||||
**Response Format:**
|
||||
Response should be in the following markdown codeblock format:
|
||||
```commit
|
||||
type: Title
|
||||
|
||||
- Detail message line 1
|
||||
- Detail message line 2
|
||||
- Detail message line 3
|
||||
```
|
||||
Only output the commit message codeblock, don't include any other text.
|
||||
|
||||
**Constraints:**
|
||||
- Exclude markdown code block indicators (```) and the placeholder \"commit_message\" from your response.
|
||||
- Follow commit message best practices:
|
||||
- Limit the title length to 50 characters.
|
||||
- Limit each summary line to 72 characters.
|
||||
|
||||
**User Input:** `{__USER_INPUT__}`
|
||||
|
||||
Determine if `{__USER_INPUT__}` contains a reference to closing an issue. If so, include the closing reference in the commit message. Otherwise, exclude it.
|
||||
|
||||
**Code Changes:**
|
||||
```
|
||||
{__DIFF__}
|
||||
```
|
||||
|
||||
Related issue:
|
||||
{__ISSUE__}
|
||||
Utilize the provided format to craft a commit message that adheres to the stipulated criteria.
|
||||
|
||||
example output:
|
||||
```commit
|
||||
feature: add update user info API
|
||||
|
||||
- add post method api /user/update
|
||||
- implement update user info logic
|
||||
```
|
||||
|
5
community/github/commit/zh/command.yml
Normal file
5
community/github/commit/zh/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: '为你选定的代码变更生成格式规范的提交信息,并通过 Git 提交。如需要可包含对应 issue 编号(例如,输入“/commit to close #12”)'
|
||||
hint: to close Issue #issue_number
|
||||
input: optional
|
||||
steps:
|
||||
- run: $devchat_python $command_path/../commit.py "$input" "chinese"
|
78
community/github/common_util.py
Normal file
78
community/github/common_util.py
Normal file
@ -0,0 +1,78 @@
|
||||
import functools
|
||||
import sys
|
||||
|
||||
from lib.chatmark import Checkbox, Form, Radio, TextEditor
|
||||
|
||||
|
||||
def create_ui_objs(ui_decls, args):
|
||||
ui_objs = []
|
||||
editors = []
|
||||
for i, ui in enumerate(ui_decls):
|
||||
editor = ui[0](args[i])
|
||||
if ui[1]:
|
||||
# this is the title of UI object
|
||||
editors.append(ui[1])
|
||||
editors.append(editor)
|
||||
ui_objs.append(editor)
|
||||
return ui_objs, editors
|
||||
|
||||
|
||||
def edit_form(uis, args):
|
||||
ui_objs, editors = create_ui_objs(uis, args)
|
||||
form = Form(editors)
|
||||
form.render()
|
||||
|
||||
values = []
|
||||
for obj in ui_objs:
|
||||
if isinstance(obj, TextEditor):
|
||||
values.append(obj.new_text)
|
||||
elif isinstance(obj, Radio):
|
||||
values.append(obj.selection)
|
||||
else:
|
||||
# TODO
|
||||
pass
|
||||
return values
|
||||
|
||||
|
||||
def editor(description):
|
||||
def decorator_edit(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
uis = wrapper.uis[::-1]
|
||||
return edit_form(uis, args)
|
||||
|
||||
if hasattr(func, "uis"):
|
||||
wrapper.uis = func.uis
|
||||
else:
|
||||
wrapper.uis = []
|
||||
wrapper.uis.append((TextEditor, description))
|
||||
return wrapper
|
||||
|
||||
return decorator_edit
|
||||
|
||||
|
||||
def ui_edit(ui_type, description):
|
||||
def decorator_edit(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
uis = wrapper.uis[::-1]
|
||||
return edit_form(uis, args)
|
||||
|
||||
if hasattr(func, "uis"):
|
||||
wrapper.uis = func.uis
|
||||
else:
|
||||
wrapper.uis = []
|
||||
ui_type_class = {"editor": TextEditor, "radio": Radio, "checkbox": Checkbox}[ui_type]
|
||||
wrapper.uis.append((ui_type_class, description))
|
||||
return wrapper
|
||||
|
||||
return decorator_edit
|
||||
|
||||
|
||||
def assert_exit(condition, message, exit_code=-1):
|
||||
if condition:
|
||||
if exit_code == 0:
|
||||
print(message, end="\n\n", flush=True)
|
||||
else:
|
||||
print(message, end="\n\n", file=sys.stderr, flush=True)
|
||||
sys.exit(exit_code)
|
19
community/github/config/README.md
Normal file
19
community/github/config/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
### config
|
||||
|
||||
配置GitHub工作流所需的设置。
|
||||
|
||||
#### 用途
|
||||
- 设置Issue仓库URL
|
||||
- 配置GitHub Token
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.config`
|
||||
|
||||
#### 操作流程
|
||||
1. 输入Issue仓库URL(可选)
|
||||
2. 输入GitHub Token
|
||||
3. 保存配置信息
|
||||
|
||||
#### 注意事项
|
||||
- GitHub Token应妥善保管,不要泄露
|
||||
- 配置信息将保存在本地文件中
|
88
community/github/config/command.py
Normal file
88
community/github/config/command.py
Normal file
@ -0,0 +1,88 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import editor # noqa: E402
|
||||
|
||||
|
||||
def read_issue_url():
|
||||
config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
if "issue_repo" in config_data:
|
||||
return config_data["issue_repo"]
|
||||
return ""
|
||||
|
||||
|
||||
def save_issue_url(issue_url):
|
||||
config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
# make dirs
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
|
||||
config_data = {}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
config_data["issue_repo"] = issue_url
|
||||
with open(config_path, "w+", encoding="utf-8") as f:
|
||||
json.dump(config_data, f, indent=4)
|
||||
|
||||
|
||||
def read_github_token():
|
||||
config_path = os.path.join(os.path.expanduser("~/.chat"), ".workflow_config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
if "github_token" in config_data:
|
||||
return config_data["github_token"]
|
||||
return ""
|
||||
|
||||
|
||||
def save_github_token(github_token):
|
||||
config_path = os.path.join(os.path.expanduser("~/.chat"), ".workflow_config.json")
|
||||
|
||||
config_data = {}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
config_data["github_token"] = github_token
|
||||
with open(config_path, "w+", encoding="utf-8") as f:
|
||||
json.dump(config_data, f, indent=4)
|
||||
|
||||
|
||||
@editor(
|
||||
"Please specify the issue's repository, "
|
||||
"If the issue is within this repository, no need to specify. "
|
||||
"Otherwise, format as: username/repository-name"
|
||||
)
|
||||
@editor("Input your github TOKEN to access github api:")
|
||||
def edit_issue(issue_url, github_token):
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
print("start config git settings ...", end="\n\n", flush=True)
|
||||
|
||||
issue_url = read_issue_url()
|
||||
github_token = read_github_token()
|
||||
|
||||
issue_url, github_token = edit_issue(issue_url, github_token)
|
||||
if issue_url:
|
||||
save_issue_url(issue_url)
|
||||
if github_token:
|
||||
save_github_token(github_token)
|
||||
else:
|
||||
print("Please specify the github token to access github api.")
|
||||
sys.exit(0)
|
||||
|
||||
print("config git settings successfully.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
4
community/github/config/command.yml
Normal file
4
community/github/config/command.yml
Normal file
@ -0,0 +1,4 @@
|
||||
description: 'Config required settings for GIT workflows.'
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py
|
587
community/github/git_api.py
Normal file
587
community/github/git_api.py
Normal file
@ -0,0 +1,587 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from lib.chatmark import TextEditor
|
||||
from lib.ide_service import IDEService
|
||||
|
||||
|
||||
def read_github_token():
|
||||
config_path = os.path.join(os.path.expanduser("~/.chat"), ".workflow_config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
if "github_token" in config_data:
|
||||
return config_data["github_token"]
|
||||
|
||||
# ask user to input github token
|
||||
server_access_token_editor = TextEditor("", "Please input your GITHUB access TOKEN to access:")
|
||||
server_access_token_editor.render()
|
||||
|
||||
server_access_token = server_access_token_editor.new_text
|
||||
if not server_access_token:
|
||||
print("Please input your GITHUB access TOKEN to continue.")
|
||||
sys.exit(-1)
|
||||
return server_access_token
|
||||
|
||||
|
||||
current_repo_dir = None
|
||||
|
||||
|
||||
def get_current_repo():
|
||||
"""
|
||||
获取当前文件所在的仓库信息
|
||||
"""
|
||||
global current_repo_dir
|
||||
|
||||
if not current_repo_dir:
|
||||
selected_data = IDEService().get_selected_range().dict()
|
||||
current_file = selected_data.get("abspath", None)
|
||||
if not current_file:
|
||||
return None
|
||||
current_dir = os.path.dirname(current_file)
|
||||
try:
|
||||
# 获取仓库根目录
|
||||
current_repo_dir = (
|
||||
subprocess.check_output(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=current_dir,
|
||||
)
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
# 如果发生错误,可能不在git仓库中
|
||||
return None
|
||||
return current_repo_dir
|
||||
|
||||
|
||||
def subprocess_check_output(*popenargs, timeout=None, **kwargs):
|
||||
# 将 current_dir 添加到 kwargs 中的 cwd 参数
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.check_output
|
||||
return subprocess.check_output(*popenargs, timeout=timeout, **kwargs)
|
||||
|
||||
|
||||
def subprocess_run(
|
||||
*popenargs, input=None, capture_output=False, timeout=None, check=False, **kwargs
|
||||
):
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.run
|
||||
return subprocess.run(
|
||||
*popenargs,
|
||||
input=input,
|
||||
capture_output=capture_output,
|
||||
timeout=timeout,
|
||||
check=check,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def subprocess_call(*popenargs, timeout=None, **kwargs):
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.call
|
||||
return subprocess.call(*popenargs, timeout=timeout, **kwargs)
|
||||
|
||||
|
||||
def subprocess_check_call(*popenargs, timeout=None, **kwargs):
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.check_call
|
||||
return subprocess.check_call(*popenargs, timeout=timeout, **kwargs)
|
||||
|
||||
|
||||
GITHUB_ACCESS_TOKEN = read_github_token()
|
||||
GITHUB_API_URL = "https://api.github.com"
|
||||
|
||||
|
||||
def create_issue(title, body):
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
data = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
}
|
||||
issue_api_url = f"https://api.github.com/repos/{get_github_repo(True)}/issues"
|
||||
response = requests.post(issue_api_url, headers=headers, data=json.dumps(data))
|
||||
|
||||
if response.status_code == 201:
|
||||
print("Issue created successfully!")
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to create issue: {response.content}", file=sys.stderr, end="\n\n")
|
||||
return None
|
||||
|
||||
|
||||
def update_issue_body(issue_url, issue_body):
|
||||
"""
|
||||
Update the body text of a GitHub issue.
|
||||
|
||||
:param issue_url: The API URL of the issue to update.
|
||||
:param issue_body: The new body text for the issue.
|
||||
"""
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
data = {
|
||||
"body": issue_body,
|
||||
}
|
||||
|
||||
issue_api_url = f"https://api.github.com/repos/{get_github_repo(True)}/issues"
|
||||
api_url = f"{issue_api_url}/{issue_url.split('/')[-1]}"
|
||||
response = requests.patch(api_url, headers=headers, data=json.dumps(data))
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Issue updated successfully!")
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to update issue: {response.status_code}")
|
||||
return None
|
||||
|
||||
|
||||
# parse sub tasks in issue body
|
||||
def parse_sub_tasks(body):
|
||||
sub_tasks = []
|
||||
lines = body.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("- ["):
|
||||
sub_tasks.append(line[2:])
|
||||
return sub_tasks
|
||||
|
||||
|
||||
def update_sub_tasks(body, tasks):
|
||||
# remove all existing tasks
|
||||
lines = body.split("\n")
|
||||
updated_body = "\n".join(line for line in lines if not line.startswith("- ["))
|
||||
|
||||
# add new tasks
|
||||
updated_body += "\n" + "\n".join(f"- {task}" for task in tasks)
|
||||
|
||||
return updated_body
|
||||
|
||||
|
||||
def update_task_issue_url(body, task, issue_url):
|
||||
# task is like:
|
||||
# [ ] task name
|
||||
# [x] task name
|
||||
# replace task name with issue url, like:
|
||||
# [ ] [task name](url)
|
||||
# [x] [task name](url)
|
||||
if task.find("] ") == -1:
|
||||
return None
|
||||
task = task[task.find("] ") + 2 :]
|
||||
return body.replace(task, f"[{task}]({issue_url})")
|
||||
|
||||
|
||||
def check_git_installed():
|
||||
"""
|
||||
Check if Git is installed on the local machine.
|
||||
|
||||
Tries to execute 'git --version' command to determine the presence of Git.
|
||||
|
||||
Returns:
|
||||
bool: True if Git is installed, False otherwise.
|
||||
"""
|
||||
try:
|
||||
subprocess_run(
|
||||
["git", "--version"],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("Git is not installed on your system.")
|
||||
return False
|
||||
|
||||
|
||||
def create_and_checkout_branch(branch_name):
|
||||
subprocess_run(["git", "checkout", "-b", branch_name], check=True)
|
||||
|
||||
|
||||
def is_issue_url(task):
|
||||
issue_url = f"https://github.com/{get_github_repo(True)}/issues"
|
||||
return task.strip().startswith(issue_url)
|
||||
|
||||
|
||||
def read_issue_by_url(issue_url):
|
||||
issue_number = issue_url.split("/")[-1]
|
||||
|
||||
# Construct the API endpoint URL
|
||||
issue_api_url = f"https://api.github.com/repos/{get_github_repo(True)}/issues"
|
||||
api_url = f"{issue_api_url}/{issue_number}"
|
||||
|
||||
# Send a GET request to the API endpoint
|
||||
headers = {
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
}
|
||||
response = requests.get(api_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_github_repo(issue_repo=False):
|
||||
try:
|
||||
config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
if os.path.exists(config_path) and issue_repo:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
if "issue_repo" in config_data:
|
||||
print(
|
||||
"current issue repo:",
|
||||
config_data["issue_repo"],
|
||||
end="\n\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return config_data["issue_repo"]
|
||||
|
||||
# 使用git命令获取当前仓库的URL
|
||||
result = subprocess_check_output(
|
||||
["git", "remote", "get-url", "origin"], stderr=subprocess.STDOUT
|
||||
).strip()
|
||||
# 将结果从bytes转换为str并提取出仓库信息
|
||||
repo_url = result.decode("utf-8")
|
||||
# 假设repo_url的格式为:https://github.com/username/repo.git
|
||||
parts = repo_url.split("/")
|
||||
repo = parts[-1].replace(".git", "")
|
||||
username = parts[-2].split(":")[-1]
|
||||
github_repo = f"{username}/{repo}"
|
||||
IDEService().ide_logging("debug", f"current github repo: {github_repo}")
|
||||
return github_repo
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e)
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
print("==> File not found...")
|
||||
return None
|
||||
|
||||
|
||||
# 获取当前分支名称
|
||||
def get_current_branch():
|
||||
try:
|
||||
# 使用git命令获取当前分支名称
|
||||
result = subprocess_check_output(
|
||||
["git", "branch", "--show-current"], stderr=subprocess.STDOUT
|
||||
).strip()
|
||||
# 将结果从bytes转换为str
|
||||
current_branch = result.decode("utf-8")
|
||||
return current_branch
|
||||
except subprocess.CalledProcessError:
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
return None
|
||||
|
||||
|
||||
def get_parent_branch():
|
||||
current_branch = get_current_branch()
|
||||
if current_branch is None:
|
||||
return None
|
||||
try:
|
||||
# 使用git命令获取当前分支的父分支引用
|
||||
result = subprocess_check_output(
|
||||
["git", "rev-parse", "--abbrev-ref", f"{current_branch}@{1}"],
|
||||
stderr=subprocess.STDOUT,
|
||||
).strip()
|
||||
# 将结果从bytes转换为str
|
||||
parent_branch_ref = result.decode("utf-8")
|
||||
print("==>", parent_branch_ref)
|
||||
if parent_branch_ref == current_branch:
|
||||
# 如果父分支引用和当前分支相同,说明当前分支可能是基于一个没有父分支的提交创建的
|
||||
return None
|
||||
# 使用git命令获取父分支的名称
|
||||
result = subprocess_check_output(
|
||||
["git", "name-rev", "--name-only", "--exclude=tags/*", parent_branch_ref],
|
||||
stderr=subprocess.STDOUT,
|
||||
).strip()
|
||||
parent_branch_name = result.decode("utf-8")
|
||||
return parent_branch_name
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e)
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
print("==> File not found...")
|
||||
return None
|
||||
|
||||
|
||||
def get_issue_info(issue_id):
|
||||
# Construct the API endpoint URL
|
||||
issue_api_url = f"https://api.github.com/repos/{get_github_repo(True)}/issues"
|
||||
api_url = f"{issue_api_url}/{issue_id}"
|
||||
|
||||
# Send a GET request to the API endpoint
|
||||
headers = {
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
}
|
||||
response = requests.get(api_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_issue_info_by_url(issue_url):
|
||||
# get issue id from issue_url
|
||||
def get_issue_id(issue_url):
|
||||
# Extract the issue id from the issue_url
|
||||
issue_id = issue_url.split("/")[-1]
|
||||
return issue_id
|
||||
|
||||
return get_issue_info(get_issue_id(issue_url))
|
||||
|
||||
|
||||
# 获取当前分支自从与base_branch分叉以来的历史提交信息
|
||||
def get_commit_messages(base_branch):
|
||||
# 找到当前分支与base_branch的分叉点
|
||||
merge_base = subprocess_run(
|
||||
["git", "merge-base", "HEAD", base_branch],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# 检查是否成功找到分叉点
|
||||
if merge_base.returncode != 0:
|
||||
raise RuntimeError(f"Error finding merge base: {merge_base.stderr.strip()}")
|
||||
|
||||
# 获取分叉点的提交哈希
|
||||
merge_base_commit = merge_base.stdout.strip()
|
||||
|
||||
# 获取从分叉点到当前分支的所有提交信息
|
||||
result = subprocess_run(
|
||||
["git", "log", f"{merge_base_commit}..HEAD"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# 检查git log命令是否成功执行
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Error retrieving commit messages: {result.stderr.strip()}")
|
||||
|
||||
# 返回提交信息列表
|
||||
return result.stdout
|
||||
|
||||
|
||||
# 创建PR
|
||||
def create_pull_request(title, body, head, base, repo_name):
|
||||
url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls"
|
||||
print("url:", url, end="\n\n")
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {"title": title, "body": body, "head": head, "base": base}
|
||||
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||
if response.status_code == 201:
|
||||
return response.json()
|
||||
print(response.text, end="\n\n", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def run_command_with_retries(command, retries=3, delay=5):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
subprocess_check_call(command)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {e}")
|
||||
if attempt < retries - 1:
|
||||
print(f"Retrying... (attempt {attempt + 1}/{retries})")
|
||||
time.sleep(delay)
|
||||
else:
|
||||
print("All retries failed.")
|
||||
return False
|
||||
|
||||
|
||||
def check_unpushed_commits():
|
||||
try:
|
||||
# 获取当前分支的本地提交和远程提交的差异
|
||||
result = subprocess_check_output(["git", "cherry", "-v"]).decode("utf-8").strip()
|
||||
# 如果结果不为空,说明存在未push的提交
|
||||
return bool(result)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error checking for unpushed commits: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def auto_push():
|
||||
# 获取当前分支名
|
||||
if not check_unpushed_commits():
|
||||
return True
|
||||
try:
|
||||
branch = (
|
||||
subprocess_check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.strip()
|
||||
.decode("utf-8")
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error getting current branch: {e}")
|
||||
return False
|
||||
|
||||
# 检查当前分支是否有对应的远程分支
|
||||
remote_branch_exists = subprocess_call(["git", "ls-remote", "--exit-code", "origin", branch])
|
||||
|
||||
push_command = ["git", "push", "origin", branch]
|
||||
if remote_branch_exists == 0:
|
||||
# 如果存在远程分支,则直接push提交
|
||||
return run_command_with_retries(push_command)
|
||||
else:
|
||||
# 如果不存在远程分支,则发布并push提交
|
||||
push_command.append("-u")
|
||||
return run_command_with_retries(push_command)
|
||||
|
||||
|
||||
def get_recently_pr(repo):
|
||||
url = f"{GITHUB_API_URL}/repos/{repo}/pulls?state=open&sort=updated"
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
print("=>:", url)
|
||||
|
||||
branch_name = get_current_branch()
|
||||
|
||||
if response.status_code == 200:
|
||||
prs = response.json()
|
||||
for pr in prs:
|
||||
if pr["head"]["ref"] == branch_name:
|
||||
return pr
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def update_pr(pr_number, title, body, repo_name):
|
||||
url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {"title": title, "body": body}
|
||||
response = requests.patch(url, headers=headers, data=json.dumps(payload))
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"PR updated successfully: {response.json()['html_url']}")
|
||||
return response.json()
|
||||
else:
|
||||
print("Failed to update PR.")
|
||||
return None
|
||||
|
||||
|
||||
def get_last_base_branch(default_branch):
|
||||
"""read last base branch from config file"""
|
||||
|
||||
def read_config_item(config_path, item):
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config.get(item)
|
||||
return None
|
||||
|
||||
project_config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
last_base_branch = read_config_item(project_config_path, "last_base_branch")
|
||||
if last_base_branch:
|
||||
return last_base_branch
|
||||
return default_branch
|
||||
|
||||
|
||||
def save_last_base_branch(base_branch=None):
|
||||
"""save last base branch to config file"""
|
||||
|
||||
def save_config_item(config_path, item, value):
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
config[item] = value
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
if not base_branch:
|
||||
base_branch = get_current_branch()
|
||||
project_config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
save_config_item(project_config_path, "last_base_branch", base_branch)
|
||||
|
||||
|
||||
def get_github_username():
|
||||
url = f"{GITHUB_API_URL}/user"
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.json()["login"]
|
||||
|
||||
|
||||
def get_github_repo_issues(
|
||||
owner_repo,
|
||||
milestone=None,
|
||||
state=None,
|
||||
assignee=None,
|
||||
creator=None,
|
||||
mentioned=None,
|
||||
labels=None,
|
||||
sort=None,
|
||||
direction=None,
|
||||
since=None,
|
||||
per_page=None,
|
||||
page=None,
|
||||
):
|
||||
url = f"{GITHUB_API_URL}/repos/{owner_repo}/issues"
|
||||
headers = {
|
||||
"Authorization": f"token {GITHUB_ACCESS_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
params = {
|
||||
"milestone": milestone,
|
||||
"state": state,
|
||||
"assignee": assignee,
|
||||
"creator": creator,
|
||||
"mentioned": mentioned,
|
||||
"labels": labels,
|
||||
"sort": sort,
|
||||
"direction": direction,
|
||||
"since": since,
|
||||
"per_page": per_page,
|
||||
"page": page,
|
||||
}
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return None
|
19
community/github/list_issue_tasks/README.md
Normal file
19
community/github/list_issue_tasks/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
### list_issue_tasks
|
||||
|
||||
列出指定Issue中的任务列表。
|
||||
|
||||
#### 用途
|
||||
- 查看Issue中的子任务
|
||||
- 跟踪任务进度
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.list_issue_tasks <issue_url>`
|
||||
|
||||
#### 操作流程
|
||||
1. 获取指定Issue的信息
|
||||
2. 解析Issue内容中的任务列表
|
||||
3. 显示任务列表
|
||||
|
||||
#### 注意事项
|
||||
- 需要提供有效的Issue URL
|
||||
- 任务应以特定格式在Issue中列出(如: - [ ] 任务描述)
|
53
community/github/list_issue_tasks/command.py
Normal file
53
community/github/list_issue_tasks/command.py
Normal file
@ -0,0 +1,53 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_json
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, editor # noqa: E402
|
||||
from git_api import create_issue # noqa: E402
|
||||
|
||||
# Function to generate issue title and body using LLM
|
||||
PROMPT = (
|
||||
"Based on the following description, "
|
||||
"suggest a title and a detailed body for a GitHub issue:\n\n"
|
||||
"Description: {description}\n\n"
|
||||
'Output format: {{"title": "<title>", "body": "<body>"}} '
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_issue_content(description):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Edit issue title:")
|
||||
@editor("Edit issue body:")
|
||||
def edit_issue(title, body):
|
||||
pass
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start new_issue ...", end="\n\n", flush=True)
|
||||
|
||||
assert_exit(len(sys.argv) < 2, "Missing argument.", exit_code=-1)
|
||||
description = sys.argv[1]
|
||||
|
||||
print("Generating issue content ...", end="\n\n", flush=True)
|
||||
issue_json_ob = generate_issue_content(description=description)
|
||||
assert_exit(not issue_json_ob, "Failed to generate issue content.", exit_code=-1)
|
||||
|
||||
issue_title, issue_body = edit_issue(issue_json_ob["title"], issue_json_ob["body"])
|
||||
assert_exit(not issue_title, "Issue creation cancelled.", exit_code=0)
|
||||
print("New Issue:", issue_title, "body:", issue_body, end="\n\n", flush=True)
|
||||
|
||||
print("Creating issue ...", end="\n\n", flush=True)
|
||||
issue = create_issue(issue_title, issue_body)
|
||||
assert_exit(not issue, "Failed to create issue.", exit_code=-1)
|
||||
print("New Issue:", issue["html_url"], end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/list_issue_tasks/command.yml
Normal file
5
community/github/list_issue_tasks/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'List issue tasks.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
21
community/github/new_branch/README.md
Normal file
21
community/github/new_branch/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
### new_branch
|
||||
|
||||
基于当前分支创建新分支并切换到新分支。
|
||||
|
||||
#### 用途
|
||||
- 快速创建新的功能或修复分支
|
||||
- 保持工作区隔离
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.new_branch <description>`
|
||||
|
||||
- description: 新分支的简短描述或相关Issue URL
|
||||
|
||||
#### 操作流程
|
||||
1. 生成多个分支名建议
|
||||
2. 用户选择或编辑分支名
|
||||
3. 创建新分支并切换
|
||||
|
||||
#### 注意事项
|
||||
- 确保当前分支的更改已提交
|
||||
- 如提供Issue URL,会自动关联Issue编号到分支名
|
95
community/github/new_branch/command.py
Normal file
95
community/github/new_branch/command.py
Normal file
@ -0,0 +1,95 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_json
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
check_git_installed,
|
||||
create_and_checkout_branch,
|
||||
is_issue_url,
|
||||
read_issue_by_url,
|
||||
save_last_base_branch,
|
||||
)
|
||||
|
||||
# Function to generate a random branch name
|
||||
PROMPT = (
|
||||
"Give me 5 different git branch names, "
|
||||
"mainly hoping to express: {task}, "
|
||||
"Good branch name should looks like: <type>/<main content>,"
|
||||
"the final result is output in JSON format, "
|
||||
'as follows: {{"names":["name1", "name2", .. "name5"]}}\n'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_branch_name(task):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="radio", description="Select a branch name")
|
||||
def select_branch_name_ui(branch_names):
|
||||
pass
|
||||
|
||||
|
||||
def select_branch_name(branch_names):
|
||||
[branch_selection] = select_branch_name_ui(branch_names)
|
||||
assert_exit(branch_selection is None, "No branch selected.", exit_code=0)
|
||||
return branch_names[branch_selection]
|
||||
|
||||
|
||||
def get_issue_or_task(task):
|
||||
if is_issue_url(task):
|
||||
issue = read_issue_by_url(task.strip())
|
||||
assert_exit(not issue, "Failed to read issue.", exit_code=-1)
|
||||
|
||||
return json.dumps(
|
||||
{"id": issue["number"], "title": issue["title"], "body": issue["body"]}
|
||||
), issue["number"]
|
||||
else:
|
||||
return task, None
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("Start create branch ...", end="\n\n", flush=True)
|
||||
|
||||
is_git_installed = check_git_installed()
|
||||
assert_exit(not is_git_installed, "Git is not installed.", exit_code=-1)
|
||||
|
||||
task = sys.argv[1]
|
||||
assert_exit(
|
||||
not task,
|
||||
"You need input something about the new branch, or input a issue url.",
|
||||
exit_code=-1,
|
||||
)
|
||||
|
||||
# read issue by url
|
||||
task, issue_id = get_issue_or_task(task)
|
||||
|
||||
# Generate 5 branch names
|
||||
print("Generating branch names ...", end="\n\n", flush=True)
|
||||
branch_names = generate_branch_name(task=task)
|
||||
assert_exit(not branch_names, "Failed to generate branch names.", exit_code=-1)
|
||||
branch_names = branch_names["names"]
|
||||
for index, branch_name in enumerate(branch_names):
|
||||
if issue_id:
|
||||
branch_names[index] = f"{branch_name}-#{issue_id}"
|
||||
|
||||
# Select branch name
|
||||
selected_branch = select_branch_name(branch_names)
|
||||
|
||||
# save base branch name
|
||||
save_last_base_branch()
|
||||
|
||||
# create and checkout branch
|
||||
print(f"Creating and checking out branch: {selected_branch}")
|
||||
create_and_checkout_branch(selected_branch)
|
||||
print("Branch has create and checkout")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/new_branch/command.yml
Normal file
5
community/github/new_branch/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new branch based current branch, and checkout new branch.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
21
community/github/new_issue/README.md
Normal file
21
community/github/new_issue/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
### new_issue
|
||||
|
||||
创建新的GitHub Issue。
|
||||
|
||||
#### 用途
|
||||
- 快速创建标准格式的Issue
|
||||
- 记录任务、bug或功能请求
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.new_issue <description>`
|
||||
|
||||
- description: Issue的简短描述
|
||||
|
||||
#### 操作流程
|
||||
1. 基于描述生成Issue标题和正文
|
||||
2. 允许用户编辑Issue内容
|
||||
3. 创建GitHub Issue
|
||||
|
||||
#### 注意事项
|
||||
- 需要有创建Issue的权限
|
||||
- 生成的内容可能需要进一步完善
|
52
community/github/new_issue/command.py
Normal file
52
community/github/new_issue/command.py
Normal file
@ -0,0 +1,52 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, editor # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import create_issue # noqa: E402
|
||||
|
||||
# Function to generate issue title and body using LLM
|
||||
PROMPT = (
|
||||
"Based on the following description, "
|
||||
"suggest a title and a detailed body for a GitHub issue:\n\n"
|
||||
"Description: {description}\n\n"
|
||||
'Output as valid JSON format: {{"title": "<title>", "body": "<body> use \\n as new line flag."}} ' # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_issue_content(description):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Edit issue title:")
|
||||
@editor("Edit issue body:")
|
||||
def edit_issue(title, body):
|
||||
pass
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start new_issue ...", end="\n\n", flush=True)
|
||||
|
||||
assert_exit(len(sys.argv) < 2, "Missing argument.", exit_code=-1)
|
||||
description = sys.argv[1]
|
||||
|
||||
print("Generating issue content ...", end="\n\n", flush=True)
|
||||
issue_json_ob = generate_issue_content(description=description)
|
||||
assert_exit(not issue_json_ob, "Failed to generate issue content.", exit_code=-1)
|
||||
|
||||
issue_title, issue_body = edit_issue(issue_json_ob["title"], issue_json_ob["body"])
|
||||
assert_exit(not issue_title, "Issue creation cancelled.", exit_code=0)
|
||||
print("New Issue:", issue_title, "body:", issue_body, end="\n\n", flush=True)
|
||||
|
||||
print("Creating issue ...", end="\n\n", flush=True)
|
||||
issue = create_issue(issue_title, issue_body)
|
||||
assert_exit(not issue, "Failed to create issue.", exit_code=-1)
|
||||
print("New Issue:", issue["html_url"], end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/new_issue/command.yml
Normal file
5
community/github/new_issue/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new issue.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
0
community/github/new_issue/from_task/README.md
Normal file
0
community/github/new_issue/from_task/README.md
Normal file
94
community/github/new_issue/from_task/command.py
Normal file
94
community/github/new_issue/from_task/command.py
Normal file
@ -0,0 +1,94 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
|
||||
from common_util import assert_exit, editor, ui_edit # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
create_issue,
|
||||
get_issue_info_by_url,
|
||||
parse_sub_tasks,
|
||||
update_issue_body,
|
||||
update_task_issue_url,
|
||||
)
|
||||
|
||||
# Function to generate issue title and body using LLM
|
||||
PROMPT = (
|
||||
"Following is parent issue content:\n"
|
||||
"{issue_content}\n\n"
|
||||
"Based on the following issue task: {task}"
|
||||
"suggest a title and a detailed body for a GitHub issue:\n\n"
|
||||
'Output format: {{"title": "<title>", "body": "<body>"}} '
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_issue_content(issue_content, task):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Edit issue title:")
|
||||
@editor("Edit issue body:")
|
||||
def edit_issue(title, body):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="radio", description="Select a task to create issue:")
|
||||
def select_task(tasks):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_url):
|
||||
issue = get_issue_info_by_url(issue_url)
|
||||
assert_exit(not issue, "Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
return {
|
||||
"id": issue["number"],
|
||||
"html_url": issue["html_url"],
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
}
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start new_issue ...", end="\n\n", flush=True)
|
||||
|
||||
assert_exit(len(sys.argv) < 2, "Missing argument.", exit_code=-1)
|
||||
issue_url = sys.argv[1]
|
||||
|
||||
old_issue = get_issue_json(issue_url)
|
||||
assert_exit(not old_issue, "Failed to retrieve issue with: {issue_url}", exit_code=-1)
|
||||
tasks = parse_sub_tasks(old_issue["body"])
|
||||
assert_exit(not tasks, "No tasks in issue body.")
|
||||
|
||||
# select task from tasks
|
||||
[task] = select_task(tasks)
|
||||
assert_exit(task is None, "No task selected.")
|
||||
task = tasks[task]
|
||||
print("task:", task, end="\n\n", flush=True)
|
||||
|
||||
print("Generating issue content ...", end="\n\n", flush=True)
|
||||
issue_json_ob = generate_issue_content(issue_content=old_issue, task=task)
|
||||
assert_exit(not issue_json_ob, "Failed to generate issue content.", exit_code=-1)
|
||||
|
||||
issue_title, issue_body = edit_issue(issue_json_ob["title"], issue_json_ob["body"])
|
||||
assert_exit(not issue_title, "Issue creation cancelled.", exit_code=0)
|
||||
print("New Issue:", issue_title, "body:", issue_body, end="\n\n", flush=True)
|
||||
|
||||
print("Creating issue ...", end="\n\n", flush=True)
|
||||
issue = create_issue(issue_title, issue_body)
|
||||
assert_exit(not issue, "Failed to create issue.", exit_code=-1)
|
||||
print("New Issue:", issue["html_url"], end="\n\n", flush=True)
|
||||
|
||||
# update issue task with new issue url
|
||||
new_body = update_task_issue_url(old_issue["body"], task, issue["html_url"])
|
||||
assert_exit(not new_body, f"{task} parse error.")
|
||||
new_issue = update_issue_body(issue_url, new_body)
|
||||
assert_exit(not new_issue, "Failed to update issue body.")
|
||||
|
||||
print("Issue tasks updated successfully!", end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/new_issue/from_task/command.yml
Normal file
5
community/github/new_issue/from_task/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new issue.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
22
community/github/new_pr/README.md
Normal file
22
community/github/new_pr/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
### new_pr
|
||||
|
||||
创建新的Pull Request。
|
||||
|
||||
#### 用途
|
||||
- 自动生成PR标题和描述
|
||||
- 简化代码审查流程
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.new_pr [additional_info]`
|
||||
|
||||
- additional_info: 可选的附加信息
|
||||
|
||||
#### 操作流程
|
||||
1. 获取当前分支信息和相关Issue
|
||||
2. 生成PR标题和描述
|
||||
3. 允许用户编辑PR内容
|
||||
4. 创建Pull Request
|
||||
|
||||
#### 注意事项
|
||||
- 确保当前分支有未合并的更改
|
||||
- 需要有创建PR的权限
|
120
community/github/new_pr/command.py
Normal file
120
community/github/new_pr/command.py
Normal file
@ -0,0 +1,120 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
auto_push,
|
||||
create_pull_request,
|
||||
get_commit_messages,
|
||||
get_current_branch,
|
||||
get_github_repo,
|
||||
get_issue_info,
|
||||
get_last_base_branch,
|
||||
save_last_base_branch,
|
||||
)
|
||||
|
||||
|
||||
# 从分支名称中提取issue id
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
# 使用LLM模型生成PR内容
|
||||
PROMPT = (
|
||||
"Create a pull request title and body based on "
|
||||
"the following issue and commit messages, if there is an "
|
||||
"issue, close that issue in PR body as <user>/<repo>#issue_id:\n"
|
||||
"Issue: {issue}\n"
|
||||
"Commits:\n{commit_messages}\n"
|
||||
"Other information:\n{user_input}\n\n"
|
||||
"The response result should format as JSON object as following:\n"
|
||||
'{{"title": "pr title", "body": "pr body"}}'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_pr_content_llm(issue, commit_message, user_input):
|
||||
pass
|
||||
|
||||
|
||||
def generate_pr_content(issue, commit_messages, user_input):
|
||||
response = generate_pr_content_llm(
|
||||
issue=json.dumps(issue), commit_messages=commit_messages, user_input=user_input
|
||||
)
|
||||
assert_exit(not response, "Failed to generate PR content.", exit_code=-1)
|
||||
return response.get("title"), response.get("body")
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit PR title:")
|
||||
@ui_edit(ui_type="editor", description="Edit PR body:")
|
||||
def edit_pr(title, body):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit base branch:")
|
||||
def edit_base_branch(base_branch):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_id):
|
||||
issue = {"id": "no issue id", "title": "", "body": ""}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, "Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"html_url": issue["html_url"],
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
print("start new_pr ...", end="\n\n", flush=True)
|
||||
|
||||
base_branch = get_last_base_branch("main")
|
||||
base_branch = edit_base_branch(base_branch)
|
||||
if isinstance(base_branch, list) and len(base_branch) > 0:
|
||||
base_branch = base_branch[0]
|
||||
save_last_base_branch(base_branch)
|
||||
|
||||
repo_name = get_github_repo()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
|
||||
# print basic info, repo_name, branch_name, issue_id
|
||||
print("repo name:", repo_name, end="\n\n")
|
||||
print("branch name:", branch_name, end="\n\n")
|
||||
print("issue id:", issue_id, end="\n\n")
|
||||
|
||||
issue = get_issue_json(issue_id)
|
||||
commit_messages = get_commit_messages(base_branch)
|
||||
|
||||
print("generating pr title and body ...", end="\n\n", flush=True)
|
||||
user_input = sys.argv[1]
|
||||
pr_title, pr_body = generate_pr_content(issue, commit_messages, user_input)
|
||||
assert_exit(not pr_title, "Failed to generate PR content.", exit_code=-1)
|
||||
|
||||
pr_title, pr_body = edit_pr(pr_title, pr_body)
|
||||
assert_exit(not pr_title, "PR creation cancelled.", exit_code=0)
|
||||
|
||||
is_push_success = auto_push()
|
||||
assert_exit(not is_push_success, "Failed to push changes.", exit_code=-1)
|
||||
|
||||
pr = create_pull_request(pr_title, pr_body, branch_name, base_branch, repo_name)
|
||||
assert_exit(not pr, "Failed to create PR.", exit_code=-1)
|
||||
|
||||
print(f"PR created successfully: {pr['html_url']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/new_pr/command.yml
Normal file
5
community/github/new_pr/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new PR.'
|
||||
input: optional
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
22
community/github/update_issue_tasks/README.md
Normal file
22
community/github/update_issue_tasks/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
### update_issue_tasks
|
||||
|
||||
更新指定Issue中的任务列表。
|
||||
|
||||
#### 用途
|
||||
- 添加、修改或删除Issue中的子任务
|
||||
- 更新任务进度
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.update_issue_tasks`
|
||||
|
||||
#### 操作流程
|
||||
1. 输入Issue URL
|
||||
2. 显示当前任务列表
|
||||
3. 用户输入更新建议
|
||||
4. 生成新的任务列表
|
||||
5. 允许用户编辑新任务列表
|
||||
6. 更新Issue内容
|
||||
|
||||
#### 注意事项
|
||||
- 需要有编辑Issue的权限
|
||||
- 小心不要删除或覆盖重要信息
|
101
community/github/update_issue_tasks/command.py
Normal file
101
community/github/update_issue_tasks/command.py
Normal file
@ -0,0 +1,101 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, editor # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
get_issue_info_by_url,
|
||||
parse_sub_tasks,
|
||||
update_issue_body,
|
||||
update_sub_tasks,
|
||||
)
|
||||
|
||||
TASKS_PROMPT = (
|
||||
"Following is my git issue content.\n"
|
||||
"{issue_data}\n\n"
|
||||
"Sub task in issue is like:- [ ] task name\n"
|
||||
"'[ ] task name' will be as sub task content\n\n"
|
||||
"Following is my idea to update sub tasks:\n"
|
||||
"{user_input}\n\n"
|
||||
"Please output all tasks in JSON format as:"
|
||||
'{{"tasks": ["[ ] task1", "[ ] task2"]}}'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=TASKS_PROMPT)
|
||||
def generate_issue_tasks(issue_data, user_input):
|
||||
pass
|
||||
|
||||
|
||||
def to_task_str(tasks):
|
||||
task_str = ""
|
||||
for task in tasks:
|
||||
task_str += task + "\n"
|
||||
return task_str
|
||||
|
||||
|
||||
@editor("Edit issue old tasks:")
|
||||
@editor("Edit issue new tasks:")
|
||||
def edit_issue_tasks(old_tasks, new_tasks):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Input ISSUE url:")
|
||||
def input_issue_url(url):
|
||||
pass
|
||||
|
||||
|
||||
@editor("How to update tasks:")
|
||||
def update_tasks_input(user_input):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_url):
|
||||
issue = get_issue_info_by_url(issue_url)
|
||||
assert_exit(not issue, "Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
return {
|
||||
"id": issue["number"],
|
||||
"html_url": issue["html_url"],
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
}
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start issue tasks update ...", end="\n\n", flush=True)
|
||||
|
||||
[issue_url] = input_issue_url("")
|
||||
assert_exit(not issue_url, "No issue url.")
|
||||
print("issue url:", issue_url, end="\n\n", flush=True)
|
||||
|
||||
issue = get_issue_json(issue_url)
|
||||
old_tasks = parse_sub_tasks(issue["body"])
|
||||
|
||||
print(f"```tasks\n{to_task_str(old_tasks)}\n```", end="\n\n", flush=True)
|
||||
|
||||
[user_input] = update_tasks_input("")
|
||||
assert_exit(not user_input, "No user input")
|
||||
|
||||
new_tasks = generate_issue_tasks(issue_data=issue, user_input=user_input)
|
||||
assert_exit(not new_tasks, "No new tasks.")
|
||||
print("new_tasks:", new_tasks, end="\n\n", flush=True)
|
||||
assert_exit(not new_tasks.get("tasks", []), "No new tasks.")
|
||||
print("new tasks:", to_task_str(new_tasks["tasks"]), end="\n\n", flush=True)
|
||||
new_tasks = new_tasks["tasks"]
|
||||
|
||||
[old_tasks, new_tasks] = edit_issue_tasks(to_task_str(old_tasks), to_task_str(new_tasks))
|
||||
assert_exit(not new_tasks, "No new tasks.")
|
||||
print("new tasks:", new_tasks, end="\n\n", flush=True)
|
||||
|
||||
new_body = update_sub_tasks(issue["body"], new_tasks.split("\n"))
|
||||
new_issue = update_issue_body(issue_url, new_body)
|
||||
assert_exit(not new_issue, "Failed to update issue body.")
|
||||
|
||||
print("Issue tasks updated successfully!", end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/update_issue_tasks/command.yml
Normal file
5
community/github/update_issue_tasks/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Update issue tasks.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
20
community/github/update_pr/README.md
Normal file
20
community/github/update_pr/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
### update_pr
|
||||
|
||||
更新现有的Pull Request。
|
||||
|
||||
#### 用途
|
||||
- 更新PR的标题和描述
|
||||
- 反映最新的代码变更
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.update_pr`
|
||||
|
||||
#### 操作流程
|
||||
1. 获取最近的PR信息
|
||||
2. 重新生成PR标题和描述
|
||||
3. 允许用户编辑PR内容
|
||||
4. 更新Pull Request
|
||||
|
||||
#### 注意事项
|
||||
- 确保有更新PR的权限
|
||||
- 更新前请确认是否有新的提交需要推送
|
122
community/github/update_pr/command.py
Normal file
122
community/github/update_pr/command.py
Normal file
@ -0,0 +1,122 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from devchat.llm import ( # noqa: E402
|
||||
chat_json,
|
||||
)
|
||||
from git_api import ( # noqa: E402
|
||||
auto_push,
|
||||
get_commit_messages,
|
||||
get_current_branch,
|
||||
get_github_repo,
|
||||
get_issue_info,
|
||||
get_last_base_branch,
|
||||
get_recently_pr,
|
||||
save_last_base_branch,
|
||||
update_pr,
|
||||
)
|
||||
|
||||
|
||||
# 从分支名称中提取issue id
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
# 使用LLM模型生成PR内容
|
||||
PROMPT = (
|
||||
"Create a pull request title and body based on "
|
||||
"the following issue and commit messages, if there is an "
|
||||
"issue, close that issue in PR body as <user>/<repo>#issue_id:\n"
|
||||
"Issue: {issue}\n"
|
||||
"Commits:\n{commit_messages}\n"
|
||||
"The response result should format as JSON object as following:\n"
|
||||
'{{"title": "pr title", "body": "pr body"}}'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_pr_content_llm(issue, commit_messages):
|
||||
pass
|
||||
|
||||
|
||||
def generate_pr_content(issue, commit_messages):
|
||||
response = generate_pr_content_llm(issue=json.dumps(issue), commit_messages=commit_messages)
|
||||
assert_exit(not response, "Failed to generate PR content.", exit_code=-1)
|
||||
return response.get("title"), response.get("body")
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit PR title:")
|
||||
@ui_edit(ui_type="editor", description="Edit PR body:")
|
||||
def edit_pr(title, body):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit base branch:")
|
||||
def edit_base_branch(base_branch):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_id):
|
||||
issue = {"id": "no issue id", "title": "", "body": ""}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, "Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"html_url": issue["html_url"],
|
||||
"title": issue["title"],
|
||||
"body": issue["body"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
print("start update_pr ...", end="\n\n", flush=True)
|
||||
|
||||
base_branch = get_last_base_branch("main")
|
||||
base_branch = edit_base_branch(base_branch)
|
||||
if isinstance(base_branch, list) and len(base_branch) > 0:
|
||||
base_branch = base_branch[0]
|
||||
save_last_base_branch(base_branch)
|
||||
|
||||
repo_name = get_github_repo()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
|
||||
# print basic info, repo_name, branch_name, issue_id
|
||||
print("repo name:", repo_name, end="\n\n")
|
||||
print("branch name:", branch_name, end="\n\n")
|
||||
print("issue id:", issue_id, end="\n\n")
|
||||
|
||||
issue = get_issue_json(issue_id)
|
||||
commit_messages = get_commit_messages(base_branch)
|
||||
|
||||
recent_pr = get_recently_pr(repo_name)
|
||||
assert_exit(not recent_pr, "Failed to get recent PR.", exit_code=-1)
|
||||
|
||||
print("generating pr title and body ...", end="\n\n", flush=True)
|
||||
pr_title, pr_body = generate_pr_content(issue, commit_messages)
|
||||
assert_exit(not pr_title, "Failed to generate PR content.", exit_code=-1)
|
||||
|
||||
pr_title, pr_body = edit_pr(pr_title, pr_body)
|
||||
assert_exit(not pr_title, "PR creation cancelled.", exit_code=0)
|
||||
|
||||
is_push_success = auto_push()
|
||||
assert_exit(not is_push_success, "Failed to push changes.", exit_code=-1)
|
||||
|
||||
pr = update_pr(recent_pr["number"], pr_title, pr_body, repo_name)
|
||||
assert_exit(not pr, "Failed to update PR.", exit_code=-1)
|
||||
|
||||
print(f"PR updated successfully: {pr['html_url']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/github/update_pr/command.yml
Normal file
5
community/github/update_pr/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Update PR.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
24
community/gitlab/code_task_summary/README.md
Normal file
24
community/gitlab/code_task_summary/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
### code_task_summary
|
||||
|
||||
根据当前分支或指定的Issue,生成代码任务摘要。
|
||||
|
||||
#### 用途
|
||||
- 自动生成简洁的代码任务描述
|
||||
- 帮助开发者快速理解任务要点
|
||||
- 用于更新项目配置或文档
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.code_task_summary [issue_url]`
|
||||
|
||||
- 如不提供issue_url,将基于当前分支名称提取Issue信息
|
||||
- 如提供issue_url,将直接使用该Issue的内容
|
||||
|
||||
#### 操作流程
|
||||
1. 获取Issue信息
|
||||
2. 生成代码任务摘要
|
||||
3. 允许用户编辑摘要
|
||||
4. 更新项目配置文件
|
||||
|
||||
#### 注意事项
|
||||
- 确保Git仓库配置正确
|
||||
- 需要有效的GitHub Token以访问API
|
124
community/gitlab/code_task_summary/command.py
Normal file
124
community/gitlab/code_task_summary/command.py
Normal file
@ -0,0 +1,124 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_json
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
check_git_installed,
|
||||
get_current_branch,
|
||||
get_gitlab_issue_repo,
|
||||
get_issue_info,
|
||||
is_issue_url,
|
||||
read_issue_by_url,
|
||||
)
|
||||
|
||||
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
# Function to generate a random branch name
|
||||
PROMPT = (
|
||||
"You are a coding engineer, required to summarize the ISSUE description into a coding task description of no more than 50 words. \n" # noqa: E501
|
||||
"The ISSUE description is as follows: {issue_body}, please summarize the corresponding coding task description.\n" # noqa: E501
|
||||
'The coding task description should be output in JSON format, in the form of: {{"summary": "code task summary"}}\n' # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_code_task_summary(issue_body):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit code task summary:")
|
||||
def edit_code_task_summary(task_summary):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_or_task(task):
|
||||
if is_issue_url(task):
|
||||
issue = read_issue_by_url(task.strip())
|
||||
assert_exit(not issue, "Failed to read issue.", exit_code=-1)
|
||||
|
||||
return json.dumps(
|
||||
{"id": issue["iid"], "title": issue["title"], "description": issue["description"]}
|
||||
)
|
||||
else:
|
||||
return task
|
||||
|
||||
|
||||
def get_issue_json(issue_id, task):
|
||||
issue = {"id": "no issue id", "title": "", "description": task}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, f"Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"web_url": issue["web_url"],
|
||||
"title": issue["title"],
|
||||
"description": issue["description"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("Start update code task summary ...", end="\n\n", flush=True)
|
||||
|
||||
is_git_installed = check_git_installed()
|
||||
assert_exit(not is_git_installed, "Git is not installed.", exit_code=-1)
|
||||
|
||||
task = sys.argv[1]
|
||||
|
||||
repo_name = get_gitlab_issue_repo()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
|
||||
# print basic info, repo_name, branch_name, issue_id
|
||||
print("repo name:", repo_name, end="\n\n")
|
||||
print("branch name:", branch_name, end="\n\n")
|
||||
print("issue id:", issue_id, end="\n\n")
|
||||
|
||||
issue = get_issue_json(issue_id, task)
|
||||
assert_exit(
|
||||
not issue["description"], f"Failed to retrieve issue with ID: {issue_id}", exit_code=-1
|
||||
)
|
||||
|
||||
# Generate 5 branch names
|
||||
print("Generating code task summary ...", end="\n\n", flush=True)
|
||||
code_task_summary = generate_code_task_summary(issue_body=issue["description"])
|
||||
assert_exit(not code_task_summary, "Failed to generate code task summary.", exit_code=-1)
|
||||
assert_exit(
|
||||
not code_task_summary.get("summary", None),
|
||||
"Failed to generate code task summary, missing summary field in result.",
|
||||
exit_code=-1,
|
||||
)
|
||||
code_task_summary = code_task_summary["summary"]
|
||||
|
||||
# Select branch name
|
||||
code_task_summary = edit_code_task_summary(code_task_summary)
|
||||
assert_exit(not code_task_summary, "Failed to edit code task summary.", exit_code=-1)
|
||||
code_task_summary = code_task_summary[0]
|
||||
|
||||
# create and checkout branch
|
||||
print("Updating code task summary to config:")
|
||||
config_file = os.path.join(".chat", "complete.config")
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, "r") as f:
|
||||
config = json.load(f)
|
||||
config["taskDescription"] = code_task_summary
|
||||
else:
|
||||
config = {"taskDescription": code_task_summary}
|
||||
with open(config_file, "w") as f:
|
||||
json.dump(config, f, indent=4)
|
||||
print("Code task summary has updated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/gitlab/code_task_summary/command.yml
Normal file
5
community/gitlab/code_task_summary/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Generate code task summary.'
|
||||
input: optional
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
1
community/gitlab/command.yml
Normal file
1
community/gitlab/command.yml
Normal file
@ -0,0 +1 @@
|
||||
description: Root of gitlab commands.
|
23
community/gitlab/commit/README.md
Normal file
23
community/gitlab/commit/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
### commit
|
||||
|
||||
自动生成提交信息并执行Git提交。
|
||||
|
||||
#### 用途
|
||||
- 生成规范的提交信息
|
||||
- 简化Git提交流程
|
||||
- 保持提交历史的一致性
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.commit [message]`
|
||||
|
||||
- message: 可选的用户输入,用于辅助生成提交信息
|
||||
|
||||
#### 操作流程
|
||||
1. 选择要提交的文件
|
||||
2. 生成提交信息
|
||||
3. 允许用户编辑提交信息
|
||||
4. 执行Git提交
|
||||
|
||||
#### 注意事项
|
||||
- 确保已选择需要提交的文件
|
||||
- 生成的提交信息可能需要进一步修改以符合项目规范
|
0
community/gitlab/commit/__init__py
Normal file
0
community/gitlab/commit/__init__py
Normal file
6
community/gitlab/commit/command.yml
Normal file
6
community/gitlab/commit/command.yml
Normal file
@ -0,0 +1,6 @@
|
||||
description: 'Writes a well-formatted commit message for selected code changes and commits them via Git. Include an issue number if desired (e.g., input "/commit to close #12").'
|
||||
hint: to close Issue #issue_number
|
||||
input: optional
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/commit.py "$input" "english"
|
494
community/gitlab/commit/commit.py
Normal file
494
community/gitlab/commit/commit.py
Normal file
@ -0,0 +1,494 @@
|
||||
# flake8: noqa: E402
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_completion_stream
|
||||
|
||||
from lib.chatmark import Button, Checkbox, Form, TextEditor
|
||||
from lib.ide_service import IDEService
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit # noqa: E402
|
||||
from git_api import get_issue_info, subprocess_check_output, subprocess_run
|
||||
|
||||
diff_too_large_message_en = (
|
||||
"Commit failed. The modified content is too long "
|
||||
"and exceeds the model's length limit. "
|
||||
"You can try to make partial changes to the file and submit multiple times. "
|
||||
"Making small changes and submitting them multiple times is a better practice."
|
||||
)
|
||||
diff_too_large_message_zh = (
|
||||
"提交失败。修改内容太长,超出模型限制长度,"
|
||||
"可以尝试选择部分修改文件多次提交,小修改多提交是更好的做法。"
|
||||
)
|
||||
|
||||
COMMIT_PROMPT_LIMIT_SIZE = 20000
|
||||
|
||||
|
||||
def extract_markdown_block(text):
|
||||
"""
|
||||
Extracts the first Markdown code block from the given text without the language specifier.
|
||||
|
||||
:param text: A string containing Markdown text
|
||||
:return: The content of the first Markdown code block, or None if not found
|
||||
"""
|
||||
# 正则表达式匹配Markdown代码块,忽略可选的语言类型标记
|
||||
pattern = r"```(?:\w+)?\s*\n(.*?)\n```"
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
|
||||
if match:
|
||||
# 返回第一个匹配的代码块内容,去除首尾的反引号和语言类型标记
|
||||
# 去除块结束标记前的一个换行符,但保留其他内容
|
||||
block_content = match.group(1)
|
||||
return block_content
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
# Read the prompt from the diffCommitMessagePrompt.txt file
|
||||
def read_prompt_from_file(filename):
|
||||
"""
|
||||
Reads the content of a file and returns it as a string.
|
||||
|
||||
This function is designed to read a prompt message from a text file.
|
||||
It expects the file to be encoded in UTF-8 and will strip any leading
|
||||
or trailing whitespace from the content of the file. If the file does
|
||||
not exist or an error occurs during reading, the function logs an error
|
||||
message and exits the script.
|
||||
|
||||
Parameters:
|
||||
- filename (str): The path to the file that contains the prompt message.
|
||||
|
||||
Returns:
|
||||
- str: The content of the file as a string.
|
||||
|
||||
Raises:
|
||||
- FileNotFoundError: If the file does not exist.
|
||||
- Exception: If any other error occurs during file reading.
|
||||
"""
|
||||
try:
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
return file.read().strip()
|
||||
except FileNotFoundError:
|
||||
IDEService().ide_logging(
|
||||
"error",
|
||||
f"File {filename} not found. "
|
||||
"Please make sure it exists in the same directory as the script.",
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
IDEService().ide_logging(
|
||||
"error", f"An error occurred while reading the file {filename}: {e}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Read the prompt content from the file
|
||||
script_path = os.path.dirname(__file__)
|
||||
PROMPT_FILENAME = os.path.join(script_path, "diffCommitMessagePrompt.txt")
|
||||
PROMPT_COMMIT_MESSAGE_BY_DIFF_USER_INPUT = read_prompt_from_file(PROMPT_FILENAME)
|
||||
prompt_commit_message_by_diff_user_input_llm_config = {
|
||||
"model": os.environ.get("LLM_MODEL", "gpt-3.5-turbo-1106")
|
||||
}
|
||||
|
||||
|
||||
language = ""
|
||||
|
||||
|
||||
def assert_value(value, message):
|
||||
"""
|
||||
判断给定的value是否为True,如果是,则输出指定的message并终止程序。
|
||||
|
||||
Args:
|
||||
value: 用于判断的值。
|
||||
message: 如果value为True时需要输出的信息。
|
||||
|
||||
Returns:
|
||||
无返回值。
|
||||
|
||||
"""
|
||||
if value:
|
||||
print(message, file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def decode_path(encoded_path):
|
||||
octal_pattern = re.compile(r"\\[0-7]{3}")
|
||||
|
||||
if octal_pattern.search(encoded_path):
|
||||
bytes_path = encoded_path.encode("utf-8").decode("unicode_escape").encode("latin1")
|
||||
decoded_path = bytes_path.decode("utf-8")
|
||||
return decoded_path
|
||||
else:
|
||||
return encoded_path
|
||||
|
||||
|
||||
def get_modified_files():
|
||||
"""
|
||||
获取当前修改文件列表以及已经staged的文件列表
|
||||
|
||||
Args:
|
||||
无
|
||||
|
||||
Returns:
|
||||
tuple: 包含两个list的元组,第一个list包含当前修改过的文件,第二个list包含已经staged的文件
|
||||
"""
|
||||
""" 获取当前修改文件列表以及已经staged的文件列表"""
|
||||
output = subprocess_check_output(["git", "status", "-s", "-u"], text=True, encoding="utf-8")
|
||||
lines = output.split("\n")
|
||||
modified_files = []
|
||||
staged_files = []
|
||||
|
||||
def strip_file_name(file_name):
|
||||
file = file_name.strip()
|
||||
if file.startswith('"'):
|
||||
file = file[1:-1]
|
||||
return file
|
||||
|
||||
for line in lines:
|
||||
if len(line) > 2:
|
||||
status, filename = line[:2], decode_path(line[3:])
|
||||
# check wether filename is a directory
|
||||
if os.path.isdir(filename):
|
||||
continue
|
||||
modified_files.append(os.path.normpath(strip_file_name(filename)))
|
||||
if status == "M " or status == "A " or status == "D ":
|
||||
staged_files.append(os.path.normpath(strip_file_name(filename)))
|
||||
return modified_files, staged_files
|
||||
|
||||
|
||||
def get_marked_files(modified_files, staged_files):
|
||||
"""
|
||||
根据给定的参数获取用户选中以供提交的文件
|
||||
|
||||
Args:
|
||||
modified_files (List[str]): 用户已修改文件列表
|
||||
staged_files (List[str]): 用户已staged文件列表
|
||||
|
||||
Returns:
|
||||
List[str]: 用户选中的文件列表
|
||||
"""
|
||||
# Create two Checkbox instances for staged and unstaged files
|
||||
staged_checkbox = Checkbox(staged_files, [True] * len(staged_files))
|
||||
|
||||
unstaged_files = [file for file in modified_files if file not in staged_files]
|
||||
unstaged_checkbox = Checkbox(unstaged_files, [False] * len(unstaged_files))
|
||||
|
||||
# Create a Form with both Checkbox instances
|
||||
form_list = []
|
||||
if len(staged_files) > 0:
|
||||
form_list.append("Staged:\n\n")
|
||||
form_list.append(staged_checkbox)
|
||||
|
||||
if len(unstaged_files) > 0:
|
||||
form_list.append("Unstaged:\n\n")
|
||||
form_list.append(unstaged_checkbox)
|
||||
|
||||
form = Form(form_list, submit_button_name="Continue")
|
||||
|
||||
# Render the Form and get user input
|
||||
form.render()
|
||||
|
||||
# Retrieve the selected files from both Checkbox instances
|
||||
staged_checkbox_selections = staged_checkbox.selections if staged_checkbox.selections else []
|
||||
unstaged_selections = unstaged_checkbox.selections if unstaged_checkbox.selections else []
|
||||
selected_staged_files = [staged_files[idx] for idx in staged_checkbox_selections]
|
||||
selected_unstaged_files = [unstaged_files[idx] for idx in unstaged_selections]
|
||||
|
||||
# Combine the selections from both checkboxes
|
||||
selected_files = selected_staged_files + selected_unstaged_files
|
||||
|
||||
return selected_files
|
||||
|
||||
|
||||
def rebuild_stage_list(user_files):
|
||||
"""
|
||||
根据用户选中文件,重新构建stage列表
|
||||
|
||||
Args:
|
||||
user_files: 用户选中的文件列表
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# Unstage all files
|
||||
subprocess_check_output(["git", "reset"])
|
||||
# Stage all user_files
|
||||
for file in user_files:
|
||||
subprocess_run(["git", "add", file])
|
||||
|
||||
|
||||
def get_diff():
|
||||
"""
|
||||
获取暂存区文件的Diff信息
|
||||
|
||||
Args:
|
||||
无
|
||||
|
||||
Returns:
|
||||
bytes: 返回bytes类型,是git diff --cached命令的输出结果
|
||||
|
||||
"""
|
||||
return subprocess_check_output(["git", "diff", "--cached"])
|
||||
|
||||
|
||||
def get_current_branch():
|
||||
try:
|
||||
# 使用git命令获取当前分支名称
|
||||
result = subprocess_check_output(
|
||||
["git", "branch", "--show-current"], stderr=subprocess.STDOUT
|
||||
).strip()
|
||||
# 将结果从bytes转换为str
|
||||
current_branch = result.decode("utf-8")
|
||||
return current_branch
|
||||
except subprocess.CalledProcessError:
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
return None
|
||||
|
||||
|
||||
def generate_commit_message_base_diff(user_input, diff, issue):
|
||||
"""
|
||||
根据diff信息,通过AI生成一个commit消息
|
||||
|
||||
Args:
|
||||
user_input (str): 用户输入的commit信息
|
||||
diff (str): 提交的diff信息
|
||||
|
||||
Returns:
|
||||
str: 生成的commit消息
|
||||
|
||||
"""
|
||||
global language
|
||||
language_prompt = "You must response commit message in chinese。\n" if language == "zh" else ""
|
||||
prompt = (
|
||||
PROMPT_COMMIT_MESSAGE_BY_DIFF_USER_INPUT.replace("{__DIFF__}", f"{diff}")
|
||||
.replace("{__USER_INPUT__}", f"{user_input + language_prompt}")
|
||||
.replace("{__ISSUE__}", f"{issue}")
|
||||
)
|
||||
|
||||
model_token_limit_error = (
|
||||
diff_too_large_message_en if language == "en" else diff_too_large_message_zh
|
||||
)
|
||||
if len(str(prompt)) > COMMIT_PROMPT_LIMIT_SIZE:
|
||||
print(model_token_limit_error, flush=True)
|
||||
sys.exit(0)
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
response = chat_completion_stream(messages, prompt_commit_message_by_diff_user_input_llm_config)
|
||||
|
||||
if (
|
||||
not response["content"]
|
||||
and response.get("error", None)
|
||||
and f"{response['error']}".find("This model's maximum context length is") > 0
|
||||
):
|
||||
print(model_token_limit_error)
|
||||
sys.exit(0)
|
||||
|
||||
assert_value(not response["content"], response.get("error", ""))
|
||||
response["content"] = extract_markdown_block(response["content"])
|
||||
return response
|
||||
|
||||
|
||||
def display_commit_message_and_commit(commit_message):
|
||||
"""
|
||||
展示提交信息并提交。
|
||||
|
||||
Args:
|
||||
commit_message: 提交信息。
|
||||
|
||||
Returns:
|
||||
None。
|
||||
|
||||
"""
|
||||
text_editor = TextEditor(commit_message, submit_button_name="Commit")
|
||||
text_editor.render()
|
||||
|
||||
new_commit_message = text_editor.new_text
|
||||
if not new_commit_message:
|
||||
return None
|
||||
return subprocess_check_output(["git", "commit", "-m", new_commit_message])
|
||||
|
||||
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
def get_issue_json(issue_id):
|
||||
issue = {"id": "no issue id", "title": "", "description": ""}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, f"Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"web_url": issue["web_url"],
|
||||
"title": issue["title"],
|
||||
"description": issue["description"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
def check_git_installed():
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "--version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
except FileNotFoundError:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
except Exception:
|
||||
print("Git is not installed on your system.", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def ask_for_push():
|
||||
"""
|
||||
询问用户是否要推送(push)更改到远程仓库
|
||||
|
||||
Returns:
|
||||
bool: 用户是否选择推送
|
||||
"""
|
||||
|
||||
print(
|
||||
"Step 3/3: Would you like to push your commit to the remote repository?",
|
||||
end="\n\n",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
button = Button(["Yes, push now", "No, I'll push later"])
|
||||
button.render()
|
||||
|
||||
return button.clicked == 0 # 如果用户点击第一个按钮(Yes),则返回True
|
||||
|
||||
|
||||
def push_changes():
|
||||
"""
|
||||
推送更改到远程仓库
|
||||
|
||||
Returns:
|
||||
bool: 推送是否成功
|
||||
"""
|
||||
try:
|
||||
current_branch = get_current_branch()
|
||||
if not current_branch:
|
||||
print(
|
||||
"Could not determine current branch. Push failed.",
|
||||
end="\n\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return False
|
||||
|
||||
print(f"Pushing changes to origin/{current_branch}...", end="\n\n", flush=True)
|
||||
result = subprocess_run(
|
||||
["git", "push", "origin", current_branch],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Push failed: {result.stderr}", end="\n\n", flush=True)
|
||||
return False
|
||||
print("Push completed successfully.", end="\n\n", flush=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Push failed: {str(e)}", end="\n\n", file=sys.stderr, flush=True)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {str(e)}", end="\n\n", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
global language
|
||||
try:
|
||||
print("Let's follow the steps below.\n\n")
|
||||
# Ensure enough command line arguments are provided
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python script.py <user_input> <language>", file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
user_input = sys.argv[1]
|
||||
language = "english"
|
||||
if len(sys.argv) > 2:
|
||||
language = sys.argv[2]
|
||||
|
||||
if not check_git_installed():
|
||||
sys.exit(-1)
|
||||
|
||||
print(
|
||||
"Step 1/3: Select the files you've changed that you wish to include in this commit, "
|
||||
"then click 'Submit'.",
|
||||
end="\n\n",
|
||||
flush=True,
|
||||
)
|
||||
modified_files, staged_files = get_modified_files()
|
||||
if len(modified_files) == 0:
|
||||
print("No files to commit.", file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
selected_files = get_marked_files(modified_files, staged_files)
|
||||
if not selected_files:
|
||||
print("No files selected, commit aborted.")
|
||||
return
|
||||
|
||||
rebuild_stage_list(selected_files)
|
||||
|
||||
print(
|
||||
"Step 2/3: Review the commit message I've drafted for you. "
|
||||
"Edit it below if needed. Then click 'Commit' to proceed with "
|
||||
"the commit using this message.",
|
||||
end="\n\n",
|
||||
flush=True,
|
||||
)
|
||||
diff = get_diff()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
issue = str(get_issue_json(issue_id))
|
||||
if branch_name:
|
||||
user_input += "\ncurrent repo branch name is:" + branch_name
|
||||
commit_message = generate_commit_message_base_diff(user_input, diff, issue)
|
||||
|
||||
# TODO
|
||||
# remove Closes #IssueNumber in commit message
|
||||
commit_message["content"] = (
|
||||
commit_message["content"]
|
||||
.replace("Closes #IssueNumber", "")
|
||||
.replace("No specific issue to close", "")
|
||||
.replace("No specific issue mentioned.", "")
|
||||
)
|
||||
|
||||
commit_result = display_commit_message_and_commit(commit_message["content"])
|
||||
if not commit_result:
|
||||
print("Commit aborted.", flush=True)
|
||||
else:
|
||||
# 添加推送步骤
|
||||
if ask_for_push():
|
||||
if not push_changes():
|
||||
print("Push failed.", flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
print("Commit completed.", flush=True)
|
||||
sys.exit(0)
|
||||
except Exception as err:
|
||||
print("Exception:", err, file=sys.stderr, flush=True)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
49
community/gitlab/commit/diffCommitMessagePrompt.txt
Normal file
49
community/gitlab/commit/diffCommitMessagePrompt.txt
Normal file
@ -0,0 +1,49 @@
|
||||
Objective:** Generate a commit message that succinctly describes the codebase changes reflected in the provided diff, while incorporating any extra context or guidance from the user.
|
||||
|
||||
**Commit Message Structure:**
|
||||
1. **Title Line:** Choose a type such as `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, and so on, and couple it with a succinct title. Use the format: `type: Title`. Only one title line is permissible.
|
||||
2. **Summary:** Summarize all adjustments concisely within a maximum of three detailed message lines. Prefix each line with a \"-\".
|
||||
3. **Closing Reference (Conditional):** Include the line `Closes #IssueNumber` only if a specific, relevant issue number has been mentioned in the user input.
|
||||
|
||||
**Response Format:**
|
||||
Response should be in the following markdown codeblock format:
|
||||
```commit
|
||||
type: Title
|
||||
|
||||
- Detail message line 1
|
||||
- Detail message line 2
|
||||
- Detail message line 3
|
||||
|
||||
Closes <#IssueNumber>
|
||||
```
|
||||
Only append the \"Closes #IssueNumber\" if the user input explicitly references an issue to close.
|
||||
Only output the commit message codeblock, don't include any other text.
|
||||
|
||||
**Constraints:**
|
||||
- Exclude markdown code block indicators (```) and the placeholder \"commit_message\" from your response.
|
||||
- Follow commit message best practices:
|
||||
- Limit the title length to 50 characters.
|
||||
- Limit each summary line to 72 characters.
|
||||
- If the precise issue number is not known or not stated by the user, do not include the closing reference.
|
||||
|
||||
**User Input:** `{__USER_INPUT__}`
|
||||
|
||||
Determine if `{__USER_INPUT__}` contains a reference to closing an issue. If so, include the closing reference in the commit message. Otherwise, exclude it.
|
||||
|
||||
**Code Changes:**
|
||||
```
|
||||
{__DIFF__}
|
||||
```
|
||||
|
||||
Related issue:
|
||||
{__ISSUE__}
|
||||
Utilize the provided format to craft a commit message that adheres to the stipulated criteria.
|
||||
|
||||
example output:
|
||||
```commit
|
||||
feature: add update user info API
|
||||
|
||||
- add post method api /user/update
|
||||
- implement update user info logic
|
||||
```
|
||||
|
5
community/gitlab/commit/zh/command.yml
Normal file
5
community/gitlab/commit/zh/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: '为你选定的代码变更生成格式规范的提交信息,并通过 Git 提交。如需要可包含对应 issue 编号(例如,输入“/commit to close #12”)'
|
||||
hint: to close Issue #issue_number
|
||||
input: optional
|
||||
steps:
|
||||
- run: $devchat_python $command_path/../commit.py "$input" "chinese"
|
78
community/gitlab/common_util.py
Normal file
78
community/gitlab/common_util.py
Normal file
@ -0,0 +1,78 @@
|
||||
import functools
|
||||
import sys
|
||||
|
||||
from lib.chatmark import Checkbox, Form, Radio, TextEditor
|
||||
|
||||
|
||||
def create_ui_objs(ui_decls, args):
|
||||
ui_objs = []
|
||||
editors = []
|
||||
for i, ui in enumerate(ui_decls):
|
||||
editor = ui[0](args[i])
|
||||
if ui[1]:
|
||||
# this is the title of UI object
|
||||
editors.append(ui[1])
|
||||
editors.append(editor)
|
||||
ui_objs.append(editor)
|
||||
return ui_objs, editors
|
||||
|
||||
|
||||
def edit_form(uis, args):
|
||||
ui_objs, editors = create_ui_objs(uis, args)
|
||||
form = Form(editors)
|
||||
form.render()
|
||||
|
||||
values = []
|
||||
for obj in ui_objs:
|
||||
if isinstance(obj, TextEditor):
|
||||
values.append(obj.new_text)
|
||||
elif isinstance(obj, Radio):
|
||||
values.append(obj.selection)
|
||||
else:
|
||||
# TODO
|
||||
pass
|
||||
return values
|
||||
|
||||
|
||||
def editor(description):
|
||||
def decorator_edit(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
uis = wrapper.uis[::-1]
|
||||
return edit_form(uis, args)
|
||||
|
||||
if hasattr(func, "uis"):
|
||||
wrapper.uis = func.uis
|
||||
else:
|
||||
wrapper.uis = []
|
||||
wrapper.uis.append((TextEditor, description))
|
||||
return wrapper
|
||||
|
||||
return decorator_edit
|
||||
|
||||
|
||||
def ui_edit(ui_type, description):
|
||||
def decorator_edit(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
uis = wrapper.uis[::-1]
|
||||
return edit_form(uis, args)
|
||||
|
||||
if hasattr(func, "uis"):
|
||||
wrapper.uis = func.uis
|
||||
else:
|
||||
wrapper.uis = []
|
||||
ui_type_class = {"editor": TextEditor, "radio": Radio, "checkbox": Checkbox}[ui_type]
|
||||
wrapper.uis.append((ui_type_class, description))
|
||||
return wrapper
|
||||
|
||||
return decorator_edit
|
||||
|
||||
|
||||
def assert_exit(condition, message, exit_code=-1):
|
||||
if condition:
|
||||
if exit_code == 0:
|
||||
print(message, end="\n\n", flush=True)
|
||||
else:
|
||||
print(message, end="\n\n", file=sys.stderr, flush=True)
|
||||
sys.exit(exit_code)
|
19
community/gitlab/config/README.md
Normal file
19
community/gitlab/config/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
### config
|
||||
|
||||
配置GitHub工作流所需的设置。
|
||||
|
||||
#### 用途
|
||||
- 设置Issue仓库URL
|
||||
- 配置GitHub Token
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.config`
|
||||
|
||||
#### 操作流程
|
||||
1. 输入Issue仓库URL(可选)
|
||||
2. 输入GitHub Token
|
||||
3. 保存配置信息
|
||||
|
||||
#### 注意事项
|
||||
- GitHub Token应妥善保管,不要泄露
|
||||
- 配置信息将保存在本地文件中
|
88
community/gitlab/config/command.py
Normal file
88
community/gitlab/config/command.py
Normal file
@ -0,0 +1,88 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import editor # noqa: E402
|
||||
|
||||
|
||||
def read_issue_url():
|
||||
config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
if "git_issue_repo" in config_data:
|
||||
return config_data["git_issue_repo"]
|
||||
return ""
|
||||
|
||||
|
||||
def save_issue_url(issue_url):
|
||||
config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
# make dirs
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
|
||||
config_data = {}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
config_data["git_issue_repo"] = issue_url
|
||||
with open(config_path, "w+", encoding="utf-8") as f:
|
||||
json.dump(config_data, f, indent=4)
|
||||
|
||||
|
||||
def read_gitlab_token():
|
||||
config_path = os.path.join(os.path.expanduser("~/.chat"), ".workflow_config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
if "gitlab_token" in config_data:
|
||||
return config_data["gitlab_token"]
|
||||
return ""
|
||||
|
||||
|
||||
def save_gitlab_token(github_token):
|
||||
config_path = os.path.join(os.path.expanduser("~/.chat"), ".workflow_config.json")
|
||||
|
||||
config_data = {}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
config_data["gitlab_token"] = github_token
|
||||
with open(config_path, "w+", encoding="utf-8") as f:
|
||||
json.dump(config_data, f, indent=4)
|
||||
|
||||
|
||||
@editor(
|
||||
"Please specify the issue's repository, "
|
||||
"If the issue is within this repository, no need to specify. "
|
||||
"Otherwise, format as: username/repository-name"
|
||||
)
|
||||
@editor("Input your github TOKEN to access github api:")
|
||||
def edit_issue(issue_url, github_token):
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
print("start config git settings ...", end="\n\n", flush=True)
|
||||
|
||||
issue_url = read_issue_url()
|
||||
github_token = read_gitlab_token()
|
||||
|
||||
issue_url, github_token = edit_issue(issue_url, github_token)
|
||||
if issue_url:
|
||||
save_issue_url(issue_url)
|
||||
if github_token:
|
||||
save_gitlab_token(github_token)
|
||||
else:
|
||||
print("Please specify the github token to access github api.")
|
||||
sys.exit(0)
|
||||
|
||||
print("config git settings successfully.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
4
community/gitlab/config/command.yml
Normal file
4
community/gitlab/config/command.yml
Normal file
@ -0,0 +1,4 @@
|
||||
description: 'Config required settings for GIT workflows.'
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py
|
611
community/gitlab/git_api.py
Normal file
611
community/gitlab/git_api.py
Normal file
@ -0,0 +1,611 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from lib.chatmark import TextEditor
|
||||
from lib.ide_service import IDEService
|
||||
|
||||
|
||||
def read_gitlab_token():
|
||||
config_path = os.path.join(os.path.expanduser("~/.chat"), ".workflow_config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
if "gitlab_token" in config_data:
|
||||
return config_data["gitlab_token"]
|
||||
|
||||
# ask user to input gitlab token
|
||||
server_access_token_editor = TextEditor("", "Please input your GitLab access TOKEN to access:")
|
||||
server_access_token_editor.render()
|
||||
|
||||
server_access_token = server_access_token_editor.new_text
|
||||
if not server_access_token:
|
||||
print("Please input your GitLab access TOKEN to continue.")
|
||||
sys.exit(-1)
|
||||
return server_access_token
|
||||
|
||||
|
||||
current_repo_dir = None
|
||||
|
||||
|
||||
def get_current_repo():
|
||||
"""
|
||||
获取当前文件所在的仓库信息
|
||||
"""
|
||||
global current_repo_dir
|
||||
|
||||
if not current_repo_dir:
|
||||
selected_data = IDEService().get_selected_range().dict()
|
||||
current_file = selected_data.get("abspath", None)
|
||||
if not current_file:
|
||||
return None
|
||||
current_dir = os.path.dirname(current_file)
|
||||
try:
|
||||
# 获取仓库根目录
|
||||
current_repo_dir = (
|
||||
subprocess.check_output(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=current_dir,
|
||||
)
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
# 如果发生错误,可能不在git仓库中
|
||||
return None
|
||||
return current_repo_dir
|
||||
|
||||
|
||||
def subprocess_check_output(*popenargs, timeout=None, **kwargs):
|
||||
# 将 current_dir 添加到 kwargs 中的 cwd 参数
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.check_output
|
||||
return subprocess.check_output(*popenargs, timeout=timeout, **kwargs)
|
||||
|
||||
|
||||
def subprocess_run(
|
||||
*popenargs, input=None, capture_output=False, timeout=None, check=False, **kwargs
|
||||
):
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.run
|
||||
return subprocess.run(
|
||||
*popenargs,
|
||||
input=input,
|
||||
capture_output=capture_output,
|
||||
timeout=timeout,
|
||||
check=check,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def subprocess_call(*popenargs, timeout=None, **kwargs):
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.call
|
||||
return subprocess.call(*popenargs, timeout=timeout, **kwargs)
|
||||
|
||||
|
||||
def subprocess_check_call(*popenargs, timeout=None, **kwargs):
|
||||
current_repo = get_current_repo()
|
||||
if current_repo:
|
||||
kwargs["cwd"] = kwargs.get("cwd", current_repo)
|
||||
|
||||
# 调用 subprocess.check_call
|
||||
return subprocess.check_call(*popenargs, timeout=timeout, **kwargs)
|
||||
|
||||
|
||||
GITLAB_ACCESS_TOKEN = read_gitlab_token()
|
||||
GITLAB_API_URL = "https://gitlab.com/api/v4"
|
||||
|
||||
|
||||
def create_issue(title, description):
|
||||
headers = {
|
||||
"Private-Token": GITLAB_ACCESS_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
data = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
}
|
||||
project_id = get_gitlab_project_id()
|
||||
issue_api_url = f"{GITLAB_API_URL}/projects/{project_id}/issues"
|
||||
response = requests.post(issue_api_url, headers=headers, json=data)
|
||||
|
||||
if response.status_code == 201:
|
||||
print("Issue created successfully!")
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to create issue: {response.content}", file=sys.stderr, end="\n\n")
|
||||
return None
|
||||
|
||||
|
||||
def update_issue_body(issue_iid, issue_body):
|
||||
headers = {
|
||||
"Private-Token": GITLAB_ACCESS_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
data = {
|
||||
"description": issue_body,
|
||||
}
|
||||
|
||||
project_id = get_gitlab_project_id()
|
||||
api_url = f"{GITLAB_API_URL}/projects/{project_id}/issues/{issue_iid}"
|
||||
response = requests.put(api_url, headers=headers, json=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Issue updated successfully!")
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to update issue: {response.status_code}")
|
||||
return None
|
||||
|
||||
|
||||
def get_gitlab_project_id():
|
||||
try:
|
||||
result = subprocess_check_output(
|
||||
["git", "remote", "get-url", "origin"], stderr=subprocess.STDOUT
|
||||
).strip()
|
||||
repo_url = result.decode("utf-8")
|
||||
print(f"Original repo URL: {repo_url}", file=sys.stderr)
|
||||
|
||||
if repo_url.startswith("git@"):
|
||||
# Handle SSH URL format
|
||||
parts = repo_url.split(":")
|
||||
project_path = parts[1].replace(".git", "")
|
||||
elif repo_url.startswith("https://"):
|
||||
# Handle HTTPS URL format
|
||||
parts = repo_url.split("/")
|
||||
project_path = "/".join(parts[3:]).replace(".git", "")
|
||||
else:
|
||||
raise ValueError(f"Unsupported Git URL format: {repo_url}")
|
||||
|
||||
print(f"Extracted project path: {project_path}", file=sys.stderr)
|
||||
encoded_project_path = requests.utils.quote(project_path, safe="")
|
||||
print(f"Encoded project path: {encoded_project_path}", file=sys.stderr)
|
||||
return encoded_project_path
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error executing git command: {e}", file=sys.stderr)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error in get_gitlab_project_id: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
# parse sub tasks in issue description
|
||||
def parse_sub_tasks(description):
|
||||
sub_tasks = []
|
||||
lines = description.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("- ["):
|
||||
sub_tasks.append(line[2:])
|
||||
return sub_tasks
|
||||
|
||||
|
||||
def update_sub_tasks(description, tasks):
|
||||
# remove all existing tasks
|
||||
lines = description.split("\n")
|
||||
updated_body = "\n".join(line for line in lines if not line.startswith("- ["))
|
||||
|
||||
# add new tasks
|
||||
updated_body += "\n" + "\n".join(f"- {task}" for task in tasks)
|
||||
|
||||
return updated_body
|
||||
|
||||
|
||||
def update_task_issue_url(description, task, issue_url):
|
||||
# task is like:
|
||||
# [ ] task name
|
||||
# [x] task name
|
||||
# replace task name with issue url, like:
|
||||
# [ ] [task name](url)
|
||||
# [x] [task name](url)
|
||||
if task.find("] ") == -1:
|
||||
return None
|
||||
task = task[task.find("] ") + 2 :]
|
||||
return description.replace(task, f"[{task}]({issue_url})")
|
||||
|
||||
|
||||
def check_git_installed():
|
||||
"""
|
||||
Check if Git is installed on the local machine.
|
||||
|
||||
Tries to execute 'git --version' command to determine the presence of Git.
|
||||
|
||||
Returns:
|
||||
bool: True if Git is installed, False otherwise.
|
||||
"""
|
||||
try:
|
||||
subprocess_run(
|
||||
["git", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("Git is not installed on your system.")
|
||||
return False
|
||||
|
||||
|
||||
def create_and_checkout_branch(branch_name):
|
||||
subprocess_run(["git", "checkout", "-b", branch_name], check=True)
|
||||
|
||||
|
||||
def is_issue_url(task):
|
||||
task = task.strip()
|
||||
|
||||
# 使用正则表达式匹配 http 或 https 开头,issues/数字 结尾的 URL
|
||||
pattern = r"^(http|https)://.*?/issues/\d+$"
|
||||
|
||||
is_issue = bool(re.match(pattern, task))
|
||||
|
||||
# print(f"Task to check: {task}", file=sys.stderr)
|
||||
# print(f"Is issue URL: {is_issue}", file=sys.stderr)
|
||||
|
||||
return is_issue
|
||||
|
||||
|
||||
def read_issue_by_url(issue_url):
|
||||
# Extract the issue number and project path from the URL
|
||||
issue_url = issue_url.replace("/-/", "/")
|
||||
parts = issue_url.split("/")
|
||||
issue_number = parts[-1]
|
||||
project_path = "/".join(
|
||||
parts[3:-2]
|
||||
) # Assumes URL format: https://gitlab.com/project/path/-/issues/number
|
||||
|
||||
# URL encode the project path
|
||||
encoded_project_path = requests.utils.quote(project_path, safe="")
|
||||
|
||||
# Construct the API endpoint URL
|
||||
api_url = f"{GITLAB_API_URL}/projects/{encoded_project_path}/issues/{issue_number}"
|
||||
|
||||
# Send a GET request to the API endpoint
|
||||
headers = {
|
||||
"Private-Token": GITLAB_ACCESS_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
response = requests.get(api_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Error fetching issue: {response.status_code}", file=sys.stderr)
|
||||
print(f"Response content: {response.text}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def get_gitlab_issue_repo(issue_repo=False):
|
||||
try:
|
||||
config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
if os.path.exists(config_path) and issue_repo:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
if "git_issue_repo" in config_data:
|
||||
issue_repo = requests.utils.quote(config_data["git_issue_repo"], safe="")
|
||||
print(
|
||||
"current issue repo:",
|
||||
config_data["git_issue_repo"],
|
||||
end="\n\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return config_data["git_issue_repo"]
|
||||
|
||||
return get_gitlab_project_id()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e)
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
print("==> File not found...")
|
||||
return None
|
||||
|
||||
|
||||
# 获取当前分支名称
|
||||
def get_current_branch():
|
||||
try:
|
||||
# 使用git命令获取当前分支名称
|
||||
result = subprocess_check_output(
|
||||
["git", "branch", "--show-current"], stderr=subprocess.STDOUT
|
||||
).strip()
|
||||
# 将结果从bytes转换为str
|
||||
current_branch = result.decode("utf-8")
|
||||
return current_branch
|
||||
except subprocess.CalledProcessError:
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
return None
|
||||
|
||||
|
||||
def get_parent_branch():
|
||||
current_branch = get_current_branch()
|
||||
if current_branch is None:
|
||||
return None
|
||||
try:
|
||||
# 使用git命令获取当前分支的父分支引用
|
||||
result = subprocess_check_output(
|
||||
["git", "rev-parse", "--abbrev-ref", f"{current_branch}@{1}"], stderr=subprocess.STDOUT
|
||||
).strip()
|
||||
# 将结果从bytes转换为str
|
||||
parent_branch_ref = result.decode("utf-8")
|
||||
if parent_branch_ref == current_branch:
|
||||
# 如果父分支引用和当前分支相同,说明当前分支可能是基于一个没有父分支的提交创建的
|
||||
return None
|
||||
# 使用git命令获取父分支的名称
|
||||
result = subprocess_check_output(
|
||||
["git", "name-rev", "--name-only", "--exclude=tags/*", parent_branch_ref],
|
||||
stderr=subprocess.STDOUT,
|
||||
).strip()
|
||||
parent_branch_name = result.decode("utf-8")
|
||||
return parent_branch_name
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e)
|
||||
# 如果发生错误,打印错误信息
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
# 如果未找到git命令,可能是没有安装git或者不在PATH中
|
||||
print("==> File not found...")
|
||||
return None
|
||||
|
||||
|
||||
def get_issue_info(issue_id):
|
||||
# 获取 GitLab 项目 ID
|
||||
project_id = get_gitlab_issue_repo()
|
||||
# 构造 GitLab API 端点 URL
|
||||
api_url = f"{GITLAB_API_URL}/projects/{project_id}/issues/{issue_id}"
|
||||
|
||||
# 发送 GET 请求到 API 端点
|
||||
headers = {
|
||||
"Private-Token": GITLAB_ACCESS_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
response = requests.get(api_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to get issue info. Status code: {response.status_code}", file=sys.stderr)
|
||||
print(f"Response content: {response.text}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def get_issue_info_by_url(issue_url):
|
||||
# get issue id from issue_url
|
||||
def get_issue_id(issue_url):
|
||||
# Extract the issue id from the issue_url
|
||||
issue_id = issue_url.split("/")[-1]
|
||||
return issue_id
|
||||
|
||||
return get_issue_info(get_issue_id(issue_url))
|
||||
|
||||
|
||||
# 获取当前分支自从与base_branch分叉以来的历史提交信息
|
||||
def get_commit_messages(base_branch):
|
||||
# 找到当前分支与base_branch的分叉点
|
||||
merge_base = subprocess_run(
|
||||
["git", "merge-base", "HEAD", base_branch],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# 检查是否成功找到分叉点
|
||||
if merge_base.returncode != 0:
|
||||
raise RuntimeError(f"Error finding merge base: {merge_base.stderr.strip()}")
|
||||
|
||||
# 获取分叉点的提交哈希
|
||||
merge_base_commit = merge_base.stdout.strip()
|
||||
|
||||
# 获取从分叉点到当前分支的所有提交信息
|
||||
result = subprocess_run(
|
||||
["git", "log", f"{merge_base_commit}..HEAD"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# 检查git log命令是否成功执行
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Error retrieving commit messages: {result.stderr.strip()}")
|
||||
|
||||
# 返回提交信息列表
|
||||
return result.stdout
|
||||
|
||||
|
||||
# 创建PR
|
||||
def create_pull_request(title, description, source_branch, target_branch, project_id):
|
||||
url = f"{GITLAB_API_URL}/projects/{project_id}/merge_requests"
|
||||
headers = {"Private-Token": GITLAB_ACCESS_TOKEN, "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"source_branch": source_branch,
|
||||
"target_branch": target_branch,
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code == 201:
|
||||
response_json = response.json()
|
||||
return response_json
|
||||
|
||||
print(response.text, end="\n\n", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def get_recently_mr(project_id):
|
||||
project_id = requests.utils.quote(project_id, safe="")
|
||||
url = (
|
||||
f"{GITLAB_API_URL}/projects/{project_id}/"
|
||||
"merge_requests?state=opened&order_by=updated_at&sort=desc"
|
||||
)
|
||||
headers = {
|
||||
"Private-Token": GITLAB_ACCESS_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
branch_name = get_current_branch()
|
||||
|
||||
if response.status_code == 200:
|
||||
mrs = response.json()
|
||||
for mr in mrs:
|
||||
if mr["source_branch"] == branch_name:
|
||||
return mr
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def run_command_with_retries(command, retries=3, delay=5):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
subprocess_check_call(command)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {e}")
|
||||
if attempt < retries - 1:
|
||||
print(f"Retrying... (attempt {attempt + 1}/{retries})")
|
||||
time.sleep(delay)
|
||||
else:
|
||||
print("All retries failed.")
|
||||
return False
|
||||
|
||||
|
||||
def update_mr(project_id, mr_iid, title, description):
|
||||
project_id = requests.utils.quote(project_id, safe="")
|
||||
url = f"{GITLAB_API_URL}/projects/{project_id}/merge_requests/{mr_iid}"
|
||||
headers = {"Private-Token": GITLAB_ACCESS_TOKEN, "Content-Type": "application/json"}
|
||||
payload = {"title": title, "description": description}
|
||||
response = requests.put(url, headers=headers, json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"MR updated successfully: {response.json()['web_url']}")
|
||||
return response.json()
|
||||
else:
|
||||
print("Failed to update MR.")
|
||||
return None
|
||||
|
||||
|
||||
def check_unpushed_commits():
|
||||
try:
|
||||
# 获取当前分支的本地提交和远程提交的差异
|
||||
result = subprocess_check_output(["git", "cherry", "-v"]).decode("utf-8").strip()
|
||||
# 如果结果不为空,说明存在未push的提交
|
||||
return bool(result)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error checking for unpushed commits: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def auto_push():
|
||||
# 获取当前分支名
|
||||
if not check_unpushed_commits():
|
||||
return True
|
||||
try:
|
||||
branch = (
|
||||
subprocess_check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.strip()
|
||||
.decode("utf-8")
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error getting current branch: {e}")
|
||||
return False
|
||||
|
||||
# 检查当前分支是否有对应的远程分支
|
||||
remote_branch_exists = subprocess_call(["git", "ls-remote", "--exit-code", "origin", branch])
|
||||
|
||||
push_command = ["git", "push", "origin", branch]
|
||||
if remote_branch_exists == 0:
|
||||
# 如果存在远程分支,则直接push提交
|
||||
return run_command_with_retries(push_command)
|
||||
else:
|
||||
# 如果不存在远程分支,则发布并push提交
|
||||
push_command.append("-u")
|
||||
return run_command_with_retries(push_command)
|
||||
|
||||
|
||||
def get_recently_pr(repo):
|
||||
url = f"{GITLAB_API_URL}/repos/{repo}/pulls?state=open&sort=updated"
|
||||
headers = {
|
||||
"Authorization": f"token {GITLAB_ACCESS_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
branch_name = get_current_branch()
|
||||
|
||||
if response.status_code == 200:
|
||||
prs = response.json()
|
||||
for pr in prs:
|
||||
if pr["head"]["ref"] == branch_name:
|
||||
return pr
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def update_pr(pr_number, title, description, repo_name):
|
||||
url = f"{GITLAB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
|
||||
headers = {"Authorization": f"token {GITLAB_ACCESS_TOKEN}", "Content-Type": "application/json"}
|
||||
payload = {"title": title, "description": description}
|
||||
response = requests.patch(url, headers=headers, data=json.dumps(payload))
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"PR updated successfully: {response.json()['web_url']}")
|
||||
return response.json()
|
||||
else:
|
||||
print("Failed to update PR.")
|
||||
return None
|
||||
|
||||
|
||||
def get_last_base_branch(default_branch):
|
||||
"""read last base branch from config file"""
|
||||
|
||||
def read_config_item(config_path, item):
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config.get(item)
|
||||
return None
|
||||
|
||||
project_config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
last_base_branch = read_config_item(project_config_path, "last_base_branch")
|
||||
if last_base_branch:
|
||||
return last_base_branch
|
||||
return default_branch
|
||||
|
||||
|
||||
def save_last_base_branch(base_branch=None):
|
||||
"""save last base branch to config file"""
|
||||
|
||||
def save_config_item(config_path, item, value):
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
config[item] = value
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
if not base_branch:
|
||||
base_branch = get_current_branch()
|
||||
project_config_path = os.path.join(os.getcwd(), ".chat", ".workflow_config.json")
|
||||
save_config_item(project_config_path, "last_base_branch", base_branch)
|
19
community/gitlab/list_issue_tasks/README.md
Normal file
19
community/gitlab/list_issue_tasks/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
### list_issue_tasks
|
||||
|
||||
列出指定Issue中的任务列表。
|
||||
|
||||
#### 用途
|
||||
- 查看Issue中的子任务
|
||||
- 跟踪任务进度
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.list_issue_tasks <issue_url>`
|
||||
|
||||
#### 操作流程
|
||||
1. 获取指定Issue的信息
|
||||
2. 解析Issue内容中的任务列表
|
||||
3. 显示任务列表
|
||||
|
||||
#### 注意事项
|
||||
- 需要提供有效的Issue URL
|
||||
- 任务应以特定格式在Issue中列出(如: - [ ] 任务描述)
|
53
community/gitlab/list_issue_tasks/command.py
Normal file
53
community/gitlab/list_issue_tasks/command.py
Normal file
@ -0,0 +1,53 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_json
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, editor # noqa: E402
|
||||
from git_api import create_issue # noqa: E402
|
||||
|
||||
# Function to generate issue title and description using LLM
|
||||
PROMPT = (
|
||||
"Based on the following description, "
|
||||
"suggest a title and a detailed description for a GitHub issue:\n\n"
|
||||
"Description: {description}\n\n"
|
||||
'Output format: {{"title": "<title>", "description": "<description>"}} '
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_issue_content(description):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Edit issue title:")
|
||||
@editor("Edit issue description:")
|
||||
def edit_issue(title, description):
|
||||
pass
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start new_issue ...", end="\n\n", flush=True)
|
||||
|
||||
assert_exit(len(sys.argv) < 2, "Missing argument.", exit_code=-1)
|
||||
description = sys.argv[1]
|
||||
|
||||
print("Generating issue content ...", end="\n\n", flush=True)
|
||||
issue_json_ob = generate_issue_content(description=description)
|
||||
assert_exit(not issue_json_ob, "Failed to generate issue content.", exit_code=-1)
|
||||
|
||||
issue_title, issue_body = edit_issue(issue_json_ob["title"], issue_json_ob["description"])
|
||||
assert_exit(not issue_title, "Issue creation cancelled.", exit_code=0)
|
||||
print("New Issue:", issue_title, "description:", issue_body, end="\n\n", flush=True)
|
||||
|
||||
print("Creating issue ...", end="\n\n", flush=True)
|
||||
issue = create_issue(issue_title, issue_body)
|
||||
assert_exit(not issue, "Failed to create issue.", exit_code=-1)
|
||||
print("New Issue:", issue["web_url"], end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/gitlab/list_issue_tasks/command.yml
Normal file
5
community/gitlab/list_issue_tasks/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'List issue tasks.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
21
community/gitlab/new_branch/README.md
Normal file
21
community/gitlab/new_branch/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
### new_branch
|
||||
|
||||
基于当前分支创建新分支并切换到新分支。
|
||||
|
||||
#### 用途
|
||||
- 快速创建新的功能或修复分支
|
||||
- 保持工作区隔离
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.new_branch <description>`
|
||||
|
||||
- description: 新分支的简短描述或相关Issue URL
|
||||
|
||||
#### 操作流程
|
||||
1. 生成多个分支名建议
|
||||
2. 用户选择或编辑分支名
|
||||
3. 创建新分支并切换
|
||||
|
||||
#### 注意事项
|
||||
- 确保当前分支的更改已提交
|
||||
- 如提供Issue URL,会自动关联Issue编号到分支名
|
95
community/gitlab/new_branch/command.py
Normal file
95
community/gitlab/new_branch/command.py
Normal file
@ -0,0 +1,95 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from devchat.llm import chat_json
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
check_git_installed,
|
||||
create_and_checkout_branch,
|
||||
is_issue_url,
|
||||
read_issue_by_url,
|
||||
save_last_base_branch,
|
||||
)
|
||||
|
||||
# Function to generate a random branch name
|
||||
PROMPT = (
|
||||
"Give me 5 different git branch names, "
|
||||
"mainly hoping to express: {task}, "
|
||||
"Good branch name should looks like: <type>/<main content>,"
|
||||
"the final result is output in JSON format, "
|
||||
'as follows: {{"names":["name1", "name2", .. "name5"]}}\n'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_branch_name(task):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="radio", description="Select a branch name")
|
||||
def select_branch_name_ui(branch_names):
|
||||
pass
|
||||
|
||||
|
||||
def select_branch_name(branch_names):
|
||||
[branch_selection] = select_branch_name_ui(branch_names)
|
||||
assert_exit(branch_selection is None, "No branch selected.", exit_code=0)
|
||||
return branch_names[branch_selection]
|
||||
|
||||
|
||||
def get_issue_or_task(task):
|
||||
if is_issue_url(task):
|
||||
issue = read_issue_by_url(task.strip())
|
||||
assert_exit(not issue, "Failed to read issue.", exit_code=-1)
|
||||
|
||||
return json.dumps(
|
||||
{"id": issue["iid"], "title": issue["title"], "description": issue["description"]}
|
||||
), issue["iid"]
|
||||
else:
|
||||
return task, None
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("Start create branch ...", end="\n\n", flush=True)
|
||||
|
||||
is_git_installed = check_git_installed()
|
||||
assert_exit(not is_git_installed, "Git is not installed.", exit_code=-1)
|
||||
|
||||
task = sys.argv[1]
|
||||
assert_exit(
|
||||
not task,
|
||||
"You need input something about the new branch, or input a issue url.",
|
||||
exit_code=-1,
|
||||
)
|
||||
|
||||
# read issue by url
|
||||
task, issue_id = get_issue_or_task(task)
|
||||
|
||||
# Generate 5 branch names
|
||||
print("Generating branch names ...", end="\n\n", flush=True)
|
||||
branch_names = generate_branch_name(task=task)
|
||||
assert_exit(not branch_names, "Failed to generate branch names.", exit_code=-1)
|
||||
branch_names = branch_names["names"]
|
||||
for index, branch_name in enumerate(branch_names):
|
||||
if issue_id:
|
||||
branch_names[index] = f"{branch_name}-#{issue_id}"
|
||||
|
||||
# Select branch name
|
||||
selected_branch = select_branch_name(branch_names)
|
||||
|
||||
# save base branch name
|
||||
save_last_base_branch()
|
||||
|
||||
# create and checkout branch
|
||||
print(f"Creating and checking out branch: {selected_branch}")
|
||||
create_and_checkout_branch(selected_branch)
|
||||
print("Branch has create and checkout")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/gitlab/new_branch/command.yml
Normal file
5
community/gitlab/new_branch/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new branch based current branch, and checkout new branch.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
21
community/gitlab/new_issue/README.md
Normal file
21
community/gitlab/new_issue/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
### new_issue
|
||||
|
||||
创建新的GitHub Issue。
|
||||
|
||||
#### 用途
|
||||
- 快速创建标准格式的Issue
|
||||
- 记录任务、bug或功能请求
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.new_issue <description>`
|
||||
|
||||
- description: Issue的简短描述
|
||||
|
||||
#### 操作流程
|
||||
1. 基于描述生成Issue标题和正文
|
||||
2. 允许用户编辑Issue内容
|
||||
3. 创建GitHub Issue
|
||||
|
||||
#### 注意事项
|
||||
- 需要有创建Issue的权限
|
||||
- 生成的内容可能需要进一步完善
|
52
community/gitlab/new_issue/command.py
Normal file
52
community/gitlab/new_issue/command.py
Normal file
@ -0,0 +1,52 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, editor # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import create_issue # noqa: E402
|
||||
|
||||
# Function to generate issue title and description using LLM
|
||||
PROMPT = (
|
||||
"Based on the following description, "
|
||||
"suggest a title and a detailed description for a GitHub issue:\n\n"
|
||||
"Description: {description}\n\n"
|
||||
'Output as valid JSON format: {{"title": "<title>", "description": "<description> use \\n as new line flag."}} ' # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_issue_content(description):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Edit issue title:")
|
||||
@editor("Edit issue description:")
|
||||
def edit_issue(title, description):
|
||||
pass
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start new_issue ...", end="\n\n", flush=True)
|
||||
|
||||
assert_exit(len(sys.argv) < 2, "Missing argument.", exit_code=-1)
|
||||
description = sys.argv[1]
|
||||
|
||||
print("Generating issue content ...", end="\n\n", flush=True)
|
||||
issue_json_ob = generate_issue_content(description=description)
|
||||
assert_exit(not issue_json_ob, "Failed to generate issue content.", exit_code=-1)
|
||||
|
||||
issue_title, issue_body = edit_issue(issue_json_ob["title"], issue_json_ob["description"])
|
||||
assert_exit(not issue_title, "Issue creation cancelled.", exit_code=0)
|
||||
print("New Issue:", issue_title, "description:", issue_body, end="\n\n", flush=True)
|
||||
|
||||
print("Creating issue ...", end="\n\n", flush=True)
|
||||
issue = create_issue(issue_title, issue_body)
|
||||
assert_exit(not issue, "Failed to create issue.", exit_code=-1)
|
||||
print("New Issue:", issue["web_url"], end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/gitlab/new_issue/command.yml
Normal file
5
community/gitlab/new_issue/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new issue.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
0
community/gitlab/new_issue/from_task/README.md
Normal file
0
community/gitlab/new_issue/from_task/README.md
Normal file
94
community/gitlab/new_issue/from_task/command.py
Normal file
94
community/gitlab/new_issue/from_task/command.py
Normal file
@ -0,0 +1,94 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
|
||||
from common_util import assert_exit, editor, ui_edit # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
create_issue,
|
||||
get_issue_info_by_url,
|
||||
parse_sub_tasks,
|
||||
update_issue_body,
|
||||
update_task_issue_url,
|
||||
)
|
||||
|
||||
# Function to generate issue title and description using LLM
|
||||
PROMPT = (
|
||||
"Following is parent issue content:\n"
|
||||
"{issue_content}\n\n"
|
||||
"Based on the following issue task: {task}"
|
||||
"suggest a title and a detailed description for a GitHub issue:\n\n"
|
||||
'Output format: {{"title": "<title>", "description": "<description>"}} '
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_issue_content(issue_content, task):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Edit issue title:")
|
||||
@editor("Edit issue description:")
|
||||
def edit_issue(title, description):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="radio", description="Select a task to create issue:")
|
||||
def select_task(tasks):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_url):
|
||||
issue = get_issue_info_by_url(issue_url)
|
||||
assert_exit(not issue, f"Failed to retrieve issue with ID: {issue_url}", exit_code=-1)
|
||||
return {
|
||||
"id": issue["iid"],
|
||||
"web_url": issue["web_url"],
|
||||
"title": issue["title"],
|
||||
"description": issue["description"],
|
||||
}
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start new_issue ...", end="\n\n", flush=True)
|
||||
|
||||
assert_exit(len(sys.argv) < 2, "Missing argument.", exit_code=-1)
|
||||
issue_url = sys.argv[1]
|
||||
|
||||
old_issue = get_issue_json(issue_url)
|
||||
assert_exit(not old_issue, "Failed to retrieve issue with: {issue_url}", exit_code=-1)
|
||||
tasks = parse_sub_tasks(old_issue["get_issue_json"])
|
||||
assert_exit(not tasks, "No tasks in issue description.")
|
||||
|
||||
# select task from tasks
|
||||
[task] = select_task(tasks)
|
||||
assert_exit(task is None, "No task selected.")
|
||||
task = tasks[task]
|
||||
print("task:", task, end="\n\n", flush=True)
|
||||
|
||||
print("Generating issue content ...", end="\n\n", flush=True)
|
||||
issue_json_ob = generate_issue_content(issue_content=old_issue, task=task)
|
||||
assert_exit(not issue_json_ob, "Failed to generate issue content.", exit_code=-1)
|
||||
|
||||
issue_title, issue_body = edit_issue(issue_json_ob["title"], issue_json_ob["description"])
|
||||
assert_exit(not issue_title, "Issue creation cancelled.", exit_code=0)
|
||||
print("New Issue:", issue_title, "description:", issue_body, end="\n\n", flush=True)
|
||||
|
||||
print("Creating issue ...", end="\n\n", flush=True)
|
||||
issue = create_issue(issue_title, issue_body)
|
||||
assert_exit(not issue, "Failed to create issue.", exit_code=-1)
|
||||
print("New Issue:", issue["web_url"], end="\n\n", flush=True)
|
||||
|
||||
# update issue task with new issue url
|
||||
new_body = update_task_issue_url(old_issue["description"], task, issue["web_url"])
|
||||
assert_exit(not new_body, f"{task} parse error.")
|
||||
new_issue = update_issue_body(issue_url, new_body)
|
||||
assert_exit(not new_issue, "Failed to update issue description.")
|
||||
|
||||
print("Issue tasks updated successfully!", end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/gitlab/new_issue/from_task/command.yml
Normal file
5
community/gitlab/new_issue/from_task/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new issue.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
22
community/gitlab/new_pr/README.md
Normal file
22
community/gitlab/new_pr/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
### new_pr
|
||||
|
||||
创建新的Pull Request。
|
||||
|
||||
#### 用途
|
||||
- 自动生成PR标题和描述
|
||||
- 简化代码审查流程
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.new_pr [additional_info]`
|
||||
|
||||
- additional_info: 可选的附加信息
|
||||
|
||||
#### 操作流程
|
||||
1. 获取当前分支信息和相关Issue
|
||||
2. 生成PR标题和描述
|
||||
3. 允许用户编辑PR内容
|
||||
4. 创建Pull Request
|
||||
|
||||
#### 注意事项
|
||||
- 确保当前分支有未合并的更改
|
||||
- 需要有创建PR的权限
|
120
community/gitlab/new_pr/command.py
Normal file
120
community/gitlab/new_pr/command.py
Normal file
@ -0,0 +1,120 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
auto_push,
|
||||
create_pull_request,
|
||||
get_commit_messages,
|
||||
get_current_branch,
|
||||
get_gitlab_issue_repo,
|
||||
get_issue_info,
|
||||
get_last_base_branch,
|
||||
save_last_base_branch,
|
||||
)
|
||||
|
||||
|
||||
# 从分支名称中提取issue id
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
# 使用LLM模型生成PR内容
|
||||
PROMPT = (
|
||||
"Create a pull request title and description based on "
|
||||
"the following issue and commit messages, if there is an "
|
||||
"issue, close that issue in PR description as <user>/<repo>#issue_id:\n"
|
||||
"Issue: {issue}\n"
|
||||
"Commits:\n{commit_messages}\n"
|
||||
"Other information:\n{user_input}\n\n"
|
||||
"The response result should format as JSON object as following:\n"
|
||||
'{{"title": "pr title", "description": "pr description"}}'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_pr_content_llm(issue, commit_message, user_input):
|
||||
pass
|
||||
|
||||
|
||||
def generate_pr_content(issue, commit_messages, user_input):
|
||||
response = generate_pr_content_llm(
|
||||
issue=json.dumps(issue), commit_messages=commit_messages, user_input=user_input
|
||||
)
|
||||
assert_exit(not response, "Failed to generate PR content.", exit_code=-1)
|
||||
return response.get("title"), response.get("description")
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit PR title:")
|
||||
@ui_edit(ui_type="editor", description="Edit PR description:")
|
||||
def edit_pr(title, description):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit base branch:")
|
||||
def edit_base_branch(base_branch):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_id):
|
||||
issue = {"id": "no issue id", "title": "", "description": ""}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, f"Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"web_url": issue["web_url"],
|
||||
"title": issue["title"],
|
||||
"description": issue["description"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
print("start new_pr ...", end="\n\n", flush=True)
|
||||
|
||||
base_branch = get_last_base_branch("main")
|
||||
base_branch = edit_base_branch(base_branch)
|
||||
if isinstance(base_branch, list) and len(base_branch) > 0:
|
||||
base_branch = base_branch[0]
|
||||
save_last_base_branch(base_branch)
|
||||
|
||||
repo_name = get_gitlab_issue_repo()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
|
||||
# print basic info, repo_name, branch_name, issue_id
|
||||
print("repo name:", repo_name, end="\n\n")
|
||||
print("branch name:", branch_name, end="\n\n")
|
||||
print("issue id:", issue_id, end="\n\n")
|
||||
|
||||
issue = get_issue_json(issue_id)
|
||||
commit_messages = get_commit_messages(base_branch)
|
||||
|
||||
print("generating pr title and description ...", end="\n\n", flush=True)
|
||||
user_input = sys.argv[1]
|
||||
pr_title, pr_body = generate_pr_content(issue, commit_messages, user_input)
|
||||
assert_exit(not pr_title, "Failed to generate PR content.", exit_code=-1)
|
||||
|
||||
pr_title, pr_body = edit_pr(pr_title, pr_body)
|
||||
assert_exit(not pr_title, "PR creation cancelled.", exit_code=0)
|
||||
|
||||
is_push_success = auto_push()
|
||||
assert_exit(not is_push_success, "Failed to push changes.", exit_code=-1)
|
||||
|
||||
pr = create_pull_request(pr_title, pr_body, branch_name, base_branch, repo_name)
|
||||
assert_exit(not pr, "Failed to create PR.", exit_code=-1)
|
||||
|
||||
print(f"PR created successfully: {pr['web_url']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/gitlab/new_pr/command.yml
Normal file
5
community/gitlab/new_pr/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Create new PR.'
|
||||
input: optional
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
22
community/gitlab/update_issue_tasks/README.md
Normal file
22
community/gitlab/update_issue_tasks/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
### update_issue_tasks
|
||||
|
||||
更新指定Issue中的任务列表。
|
||||
|
||||
#### 用途
|
||||
- 添加、修改或删除Issue中的子任务
|
||||
- 更新任务进度
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.update_issue_tasks`
|
||||
|
||||
#### 操作流程
|
||||
1. 输入Issue URL
|
||||
2. 显示当前任务列表
|
||||
3. 用户输入更新建议
|
||||
4. 生成新的任务列表
|
||||
5. 允许用户编辑新任务列表
|
||||
6. 更新Issue内容
|
||||
|
||||
#### 注意事项
|
||||
- 需要有编辑Issue的权限
|
||||
- 小心不要删除或覆盖重要信息
|
101
community/gitlab/update_issue_tasks/command.py
Normal file
101
community/gitlab/update_issue_tasks/command.py
Normal file
@ -0,0 +1,101 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from common_util import assert_exit, editor # noqa: E402
|
||||
from devchat.llm import chat_json # noqa: E402
|
||||
from git_api import ( # noqa: E402
|
||||
get_issue_info_by_url,
|
||||
parse_sub_tasks,
|
||||
update_issue_body,
|
||||
update_sub_tasks,
|
||||
)
|
||||
|
||||
TASKS_PROMPT = (
|
||||
"Following is my git issue content.\n"
|
||||
"{issue_data}\n\n"
|
||||
"Sub task in issue is like:- [ ] task name\n"
|
||||
"'[ ] task name' will be as sub task content\n\n"
|
||||
"Following is my idea to update sub tasks:\n"
|
||||
"{user_input}\n\n"
|
||||
"Please output all tasks in JSON format as:"
|
||||
'{{"tasks": ["[ ] task1", "[ ] task2"]}}'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=TASKS_PROMPT)
|
||||
def generate_issue_tasks(issue_data, user_input):
|
||||
pass
|
||||
|
||||
|
||||
def to_task_str(tasks):
|
||||
task_str = ""
|
||||
for task in tasks:
|
||||
task_str += task + "\n"
|
||||
return task_str
|
||||
|
||||
|
||||
@editor("Edit issue old tasks:")
|
||||
@editor("Edit issue new tasks:")
|
||||
def edit_issue_tasks(old_tasks, new_tasks):
|
||||
pass
|
||||
|
||||
|
||||
@editor("Input ISSUE url:")
|
||||
def input_issue_url(url):
|
||||
pass
|
||||
|
||||
|
||||
@editor("How to update tasks:")
|
||||
def update_tasks_input(user_input):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_url):
|
||||
issue = get_issue_info_by_url(issue_url)
|
||||
assert_exit(not issue, f"Failed to retrieve issue with ID: {issue_url}", exit_code=-1)
|
||||
return {
|
||||
"id": issue["iid"],
|
||||
"web_url": issue["web_url"],
|
||||
"title": issue["title"],
|
||||
"description": issue["description"],
|
||||
}
|
||||
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
print("start issue tasks update ...", end="\n\n", flush=True)
|
||||
|
||||
[issue_url] = input_issue_url("")
|
||||
assert_exit(not issue_url, "No issue url.")
|
||||
print("issue url:", issue_url, end="\n\n", flush=True)
|
||||
|
||||
issue = get_issue_json(issue_url)
|
||||
old_tasks = parse_sub_tasks(issue["description"])
|
||||
|
||||
print(f"```tasks\n{to_task_str(old_tasks)}\n```", end="\n\n", flush=True)
|
||||
|
||||
[user_input] = update_tasks_input("")
|
||||
assert_exit(not user_input, "No user input")
|
||||
|
||||
new_tasks = generate_issue_tasks(issue_data=issue, user_input=user_input)
|
||||
assert_exit(not new_tasks, "No new tasks.")
|
||||
print("new_tasks:", new_tasks, end="\n\n", flush=True)
|
||||
assert_exit(not new_tasks.get("tasks", []), "No new tasks.")
|
||||
print("new tasks:", to_task_str(new_tasks["tasks"]), end="\n\n", flush=True)
|
||||
new_tasks = new_tasks["tasks"]
|
||||
|
||||
[old_tasks, new_tasks] = edit_issue_tasks(to_task_str(old_tasks), to_task_str(new_tasks))
|
||||
assert_exit(not new_tasks, "No new tasks.")
|
||||
print("new tasks:", new_tasks, end="\n\n", flush=True)
|
||||
|
||||
new_body = update_sub_tasks(issue["description"], new_tasks.split("\n"))
|
||||
new_issue = update_issue_body(issue_url, new_body)
|
||||
assert_exit(not new_issue, "Failed to update issue description.")
|
||||
|
||||
print("Issue tasks updated successfully!", end="\n\n", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
5
community/gitlab/update_issue_tasks/command.yml
Normal file
5
community/gitlab/update_issue_tasks/command.yml
Normal file
@ -0,0 +1,5 @@
|
||||
description: 'Update issue tasks.'
|
||||
input: required
|
||||
help: README.md
|
||||
steps:
|
||||
- run: $devchat_python $command_path/command.py "$input"
|
20
community/gitlab/update_pr/README.md
Normal file
20
community/gitlab/update_pr/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
### update_pr
|
||||
|
||||
更新现有的Pull Request。
|
||||
|
||||
#### 用途
|
||||
- 更新PR的标题和描述
|
||||
- 反映最新的代码变更
|
||||
|
||||
#### 使用方法
|
||||
执行命令: `/github.update_pr`
|
||||
|
||||
#### 操作流程
|
||||
1. 获取最近的PR信息
|
||||
2. 重新生成PR标题和描述
|
||||
3. 允许用户编辑PR内容
|
||||
4. 更新Pull Request
|
||||
|
||||
#### 注意事项
|
||||
- 确保有更新PR的权限
|
||||
- 更新前请确认是否有新的提交需要推送
|
122
community/gitlab/update_pr/command.py
Normal file
122
community/gitlab/update_pr/command.py
Normal file
@ -0,0 +1,122 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
|
||||
from common_util import assert_exit, ui_edit # noqa: E402
|
||||
from devchat.llm import ( # noqa: E402
|
||||
chat_json,
|
||||
)
|
||||
from git_api import ( # noqa: E402
|
||||
auto_push,
|
||||
get_commit_messages,
|
||||
get_current_branch,
|
||||
get_gitlab_issue_repo,
|
||||
get_issue_info,
|
||||
get_last_base_branch,
|
||||
get_recently_pr,
|
||||
save_last_base_branch,
|
||||
update_pr,
|
||||
)
|
||||
|
||||
|
||||
# 从分支名称中提取issue id
|
||||
def extract_issue_id(branch_name):
|
||||
if "#" in branch_name:
|
||||
return branch_name.split("#")[-1]
|
||||
return None
|
||||
|
||||
|
||||
# 使用LLM模型生成PR内容
|
||||
PROMPT = (
|
||||
"Create a pull request title and description based on "
|
||||
"the following issue and commit messages, if there is an "
|
||||
"issue, close that issue in PR description as <user>/<repo>#issue_id:\n"
|
||||
"Issue: {issue}\n"
|
||||
"Commits:\n{commit_messages}\n"
|
||||
"The response result should format as JSON object as following:\n"
|
||||
'{{"title": "pr title", "description": "pr description"}}'
|
||||
)
|
||||
|
||||
|
||||
@chat_json(prompt=PROMPT)
|
||||
def generate_pr_content_llm(issue, commit_messages):
|
||||
pass
|
||||
|
||||
|
||||
def generate_pr_content(issue, commit_messages):
|
||||
response = generate_pr_content_llm(issue=json.dumps(issue), commit_messages=commit_messages)
|
||||
assert_exit(not response, "Failed to generate PR content.", exit_code=-1)
|
||||
return response.get("title"), response.get("description")
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit PR title:")
|
||||
@ui_edit(ui_type="editor", description="Edit PR description:")
|
||||
def edit_pr(title, description):
|
||||
pass
|
||||
|
||||
|
||||
@ui_edit(ui_type="editor", description="Edit base branch:")
|
||||
def edit_base_branch(base_branch):
|
||||
pass
|
||||
|
||||
|
||||
def get_issue_json(issue_id):
|
||||
issue = {"id": "no issue id", "title": "", "description": ""}
|
||||
if issue_id:
|
||||
issue = get_issue_info(issue_id)
|
||||
assert_exit(not issue, f"Failed to retrieve issue with ID: {issue_id}", exit_code=-1)
|
||||
issue = {
|
||||
"id": issue_id,
|
||||
"web_url": issue["web_url"],
|
||||
"title": issue["title"],
|
||||
"description": issue["description"],
|
||||
}
|
||||
return issue
|
||||
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
print("start update_pr ...", end="\n\n", flush=True)
|
||||
|
||||
base_branch = get_last_base_branch("main")
|
||||
base_branch = edit_base_branch(base_branch)
|
||||
if isinstance(base_branch, list) and len(base_branch) > 0:
|
||||
base_branch = base_branch[0]
|
||||
save_last_base_branch(base_branch)
|
||||
|
||||
repo_name = get_gitlab_issue_repo()
|
||||
branch_name = get_current_branch()
|
||||
issue_id = extract_issue_id(branch_name)
|
||||
|
||||
# print basic info, repo_name, branch_name, issue_id
|
||||
print("repo name:", repo_name, end="\n\n")
|
||||
print("branch name:", branch_name, end="\n\n")
|
||||
print("issue id:", issue_id, end="\n\n")
|
||||
|
||||
issue = get_issue_json(issue_id)
|
||||
commit_messages = get_commit_messages(base_branch)
|
||||
|
||||
recent_pr = get_recently_pr(repo_name)
|
||||
assert_exit(not recent_pr, "Failed to get recent PR.", exit_code=-1)
|
||||
|
||||
print("generating pr title and description ...", end="\n\n", flush=True)
|
||||
pr_title, pr_body = generate_pr_content(issue, commit_messages)
|
||||
assert_exit(not pr_title, "Failed to generate PR content.", exit_code=-1)
|
||||
|
||||
pr_title, pr_body = edit_pr(pr_title, pr_body)
|
||||
assert_exit(not pr_title, "PR creation cancelled.", exit_code=0)
|
||||
|
||||
is_push_success = auto_push()
|
||||
assert_exit(not is_push_success, "Failed to push changes.", exit_code=-1)
|
||||
|
||||
pr = update_pr(recent_pr["iid"], pr_title, pr_body, repo_name)
|
||||
assert_exit(not pr, "Failed to update PR.", exit_code=-1)
|
||||
|
||||
print(f"PR updated successfully: {pr['web_url']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user