mirror of
https://gitea.com/gitea/tea.git
synced 2026-04-26 02:03:30 +02:00
Compare commits
464 Commits
release/v0
...
v0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63bc90ea52 | ||
|
|
9e0a6203ae | ||
|
|
84ecd16f9c | ||
|
|
53e53e1067 | ||
|
|
0489d8c275 | ||
|
|
f538c05282 | ||
|
|
662e339bf9 | ||
|
|
5bb73667d1 | ||
|
|
f329f6fab2 | ||
|
|
366069315f | ||
|
|
1e13681663 | ||
|
|
bfbec3fc00 | ||
|
|
e31a167e54 | ||
|
|
6a7c3e4efa | ||
|
|
b05e03416b | ||
|
|
21881525a8 | ||
|
|
9a462247bd | ||
|
|
5f74fb37df | ||
|
|
ec658cfc33 | ||
|
|
cb9824b451 | ||
|
|
a531faa626 | ||
|
|
302c946cb8 | ||
|
|
0346e1cbb5 | ||
|
|
cd4051ed38 | ||
|
|
c797624fcf | ||
|
|
3372c9ec59 | ||
|
|
1ac8492ac7 | ||
|
|
d019f0dd72 | ||
|
|
c031db2413 | ||
|
|
e3c550ff22 | ||
|
|
fab70f83c1 | ||
|
|
0b1147bfc0 | ||
|
|
93d4d3cc55 | ||
|
|
bdf15a57be | ||
|
|
87c8c3d6e0 | ||
|
|
dfd400f15b | ||
|
|
2152d99f2d | ||
|
|
ea795775af | ||
|
|
1093ef1524 | ||
|
|
873a44f897 | ||
|
|
47f74ea696 | ||
|
|
59656dfcd2 | ||
|
|
e644cc49d4 | ||
|
|
3595f8f89d | ||
|
|
49a9032d8a | ||
|
|
982adb4d02 | ||
|
|
29488a1f46 | ||
|
|
a47ac265d2 | ||
|
|
037d1aad23 | ||
|
|
e5342660fa | ||
|
|
233ffe4508 | ||
|
|
ae9eb4f2c0 | ||
|
|
0d5bf60632 | ||
|
|
82d8a14c73 | ||
|
|
6414a5e00e | ||
|
|
864face284 | ||
|
|
383c5fdc03 | ||
|
|
7801310a18 | ||
|
|
c2180048a0 | ||
|
|
629872d1e9 | ||
|
|
0be14de5c2 | ||
|
|
4f8cb7ef19 | ||
|
|
f638dba99b | ||
|
|
20da414145 | ||
|
|
ae740a66e8 | ||
|
|
c2e9265dae | ||
|
|
45260e1a1f | ||
|
|
7ab3366220 | ||
|
|
68b9620b8c | ||
|
|
e961a8f01d | ||
|
|
f59430a42a | ||
|
|
7e2e7ee809 | ||
|
|
1d1d9197ee | ||
|
|
f6d4b5fa4f | ||
|
|
016e068c60 | ||
|
|
587b31503d | ||
|
|
4877f181fb | ||
|
|
81481f8f9d | ||
|
|
3495ec5ed4 | ||
|
|
7a5c260268 | ||
|
|
90f8624ae7 | ||
|
|
61d4e571a7 | ||
|
|
4f33146b70 | ||
|
|
08b83986dd | ||
|
|
6acb29efd7 | ||
|
|
4f513ca3e3 | ||
|
|
cc20b52ab3 | ||
|
|
2ca114e309 | ||
|
|
45771265c4 | ||
|
|
8faa1d33f4 | ||
|
|
ddf5c0a5bb | ||
|
|
d3c73cd5dc | ||
|
|
6c958eec99 | ||
|
|
d531c6fdb0 | ||
|
|
cd58296995 | ||
|
|
b74405530a | ||
|
|
8876fe3cb8 | ||
|
|
07ca1ba106 | ||
|
|
d643e94a69 | ||
|
|
d2ccead88b | ||
|
|
449b2e3117 | ||
|
|
9e8c71e13e | ||
|
|
2ddb3bd4a1 | ||
|
|
4c00b8b571 | ||
|
|
c0eb30af03 | ||
|
|
e462acfcd6 | ||
|
|
ee111d7c12 | ||
|
|
5f35cebcf1 | ||
|
|
a010c9bc7f | ||
|
|
ab4ad92d40 | ||
|
|
15052b4dcc | ||
|
|
8212d5f527 | ||
|
|
d536242aa9 | ||
|
|
ffff540aa7 | ||
|
|
f09d6ca46b | ||
|
|
0e54bae0c4 | ||
|
|
5420af1dfa | ||
|
|
177b7397f3 | ||
|
|
11ec662434 | ||
|
|
8b1601c955 | ||
|
|
213f976dcf | ||
|
|
59b35fecfe | ||
|
|
d4887f6b9d | ||
|
|
c5f398dcd2 | ||
|
|
9cb0d8baa1 | ||
|
|
662d441e3a | ||
|
|
894646a83c | ||
|
|
4adc925aa1 | ||
|
|
affef1980e | ||
|
|
6efa25d0f7 | ||
|
|
2e389349e1 | ||
|
|
2f9bc0f8d9 | ||
|
|
2e4422577b | ||
|
|
1ea89f3409 | ||
|
|
d2c9df3b17 | ||
|
|
21ede75fee | ||
|
|
d633e3018d | ||
|
|
3447e0b6ab | ||
|
|
1681af8a3e | ||
|
|
62dc1dde95 | ||
|
|
e82dd9e08d | ||
|
|
9785f6acf7 | ||
|
|
5d78e27542 | ||
|
|
e2c199fcd5 | ||
|
|
9888ca69b2 | ||
|
|
57e3400f0f | ||
|
|
60636cd7d8 | ||
|
|
47ee397110 | ||
|
|
b4655aefc4 | ||
|
|
aba2dadcfd | ||
|
|
c2ddda6800 | ||
|
|
fe1928679a | ||
|
|
681e665a59 | ||
|
|
72790aa50a | ||
|
|
1c24e199f7 | ||
|
|
8efa1aaf1f | ||
|
|
bc29fa5c9c | ||
|
|
f3fe2a0532 | ||
|
|
54b3f8e5b2 | ||
|
|
4e0fab6e7f | ||
|
|
6f924df8ac | ||
|
|
66927f8972 | ||
|
|
9de3fc8667 | ||
|
|
2fb291c6d4 | ||
|
|
637b7ea010 | ||
|
|
f1cad9dbb1 | ||
|
|
f2955a468e | ||
|
|
54c198cc84 | ||
|
|
3447be6a68 | ||
|
|
88628368f6 | ||
|
|
768a6fd844 | ||
|
|
eb204541f9 | ||
|
|
a9293e2f8b | ||
|
|
3cef822f22 | ||
|
|
f9a08f8d3b | ||
|
|
10e53345ed | ||
|
|
09aed015f2 | ||
|
|
b7191bf6e9 | ||
|
|
2984ad4964 | ||
|
|
a35bf931ae | ||
|
|
edf9f23390 | ||
|
|
0297738b33 | ||
|
|
1983c3bbd9 | ||
|
|
d289c290bb | ||
|
|
100c5a9eee | ||
|
|
4ba3139224 | ||
|
|
dd80463cf5 | ||
|
|
93480ac98e | ||
|
|
f7e2c6f89b | ||
|
|
493b29afbf | ||
|
|
723b0553c5 | ||
|
|
4c0cef090d | ||
|
|
20479663f0 | ||
|
|
22370698bf | ||
|
|
6d5e8d3b58 | ||
|
|
18abf24dca | ||
|
|
7cb6a84098 | ||
|
|
7e700e36f8 | ||
|
|
61bdc14184 | ||
|
|
16b37cc6da | ||
|
|
b5d7119ab4 | ||
|
|
610d04fc85 | ||
|
|
6696e945c6 | ||
|
|
5158adbcf2 | ||
|
|
6e473ec601 | ||
|
|
115f25e85f | ||
|
|
656d48ec8d | ||
|
|
2cb450401c | ||
|
|
5236a89d4c | ||
|
|
48d2997a63 | ||
|
|
75a7d4b7cc | ||
|
|
1e3043ce6f | ||
|
|
218b991069 | ||
|
|
16a3594474 | ||
|
|
b1b31da7c8 | ||
|
|
36672485d1 | ||
|
|
4fedaaafe1 | ||
|
|
6ae3718574 | ||
|
|
c4f6231c49 | ||
|
|
76f7f901fc | ||
|
|
005beca233 | ||
|
|
e62713f42a | ||
|
|
c8c8e9758b | ||
|
|
d15af88f83 | ||
|
|
c74177556b | ||
|
|
02836d8e52 | ||
|
|
b8ad55117a | ||
|
|
fb4eb8be9c | ||
|
|
81b784d7a5 | ||
|
|
649b0b1272 | ||
|
|
3c9552c531 | ||
|
|
f28ddccd90 | ||
|
|
5beb5bf56b | ||
|
|
17e09a3c2d | ||
|
|
58962cbb65 | ||
|
|
7029e7e8c5 | ||
|
|
3e8047dfe6 | ||
|
|
897dc50bfe | ||
|
|
2f1ca85b25 | ||
|
|
e6a6aa6f49 | ||
|
|
bcda543bb8 | ||
|
|
9e5db56dcf | ||
|
|
92d49e383d | ||
|
|
2a666c84cb | ||
|
|
32b9f5e836 | ||
|
|
33442fd05f | ||
|
|
f2cf5b7979 | ||
|
|
d80c8ceffd | ||
|
|
e3b4d68af7 | ||
|
|
0df93dd47a | ||
|
|
66b4decdd1 | ||
|
|
9e1919ec0a | ||
|
|
615cd31eb2 | ||
|
|
b868d30434 | ||
|
|
2d57e07dc7 | ||
|
|
3343fedc17 | ||
|
|
f9c67f35a6 | ||
|
|
dd8d7fb30f | ||
|
|
17453fd7cb | ||
|
|
fe4551142a | ||
|
|
10bd00d066 | ||
|
|
0869c15a6c | ||
|
|
e4e8eb07d2 | ||
|
|
620f236723 | ||
|
|
77837e909e | ||
|
|
5582f92df4 | ||
|
|
c72c6c0679 | ||
|
|
b2d845b8c7 | ||
|
|
07039b82e3 | ||
|
|
7040c350ae | ||
|
|
b02263adb0 | ||
|
|
4915862b95 | ||
|
|
cd6a7b35c3 | ||
|
|
abcaa58312 | ||
|
|
d9da952e96 | ||
|
|
7f335a2b9c | ||
|
|
b510756438 | ||
|
|
3640bbac5e | ||
|
|
b705188599 | ||
|
|
cfce25f0a7 | ||
|
|
e783f4ea4a | ||
|
|
342b573d45 | ||
|
|
1a6d9b343a | ||
|
|
faa9858a7e | ||
|
|
e51eb1a00e | ||
|
|
6a848cb72a | ||
|
|
2a8c1daa67 | ||
|
|
54b535a527 | ||
|
|
c1c7870ceb | ||
|
|
a37377d181 | ||
|
|
15457f1770 | ||
|
|
4487213581 | ||
|
|
6a4ba6a689 | ||
|
|
4ee5ce4b52 | ||
|
|
6c9b2f8745 | ||
|
|
1a256291dc | ||
|
|
832136b6d4 | ||
|
|
99e49991bb | ||
|
|
bbb287e29e | ||
|
|
5e7c702e07 | ||
|
|
b8dbf899d2 | ||
|
|
0b8be54186 | ||
|
|
2b1bca9e5d | ||
|
|
d5a258213d | ||
|
|
f83f579dea | ||
|
|
65535bd948 | ||
|
|
02f5f15269 | ||
|
|
883a27b14e | ||
|
|
e54b32493d | ||
|
|
329200b1ef | ||
|
|
6663d9f19b | ||
|
|
d06f35482e | ||
|
|
9ab36c55fa | ||
|
|
40e606561f | ||
|
|
0970b94552 | ||
|
|
dda94a5dea | ||
|
|
16ba594a28 | ||
|
|
d8f4273ed0 | ||
|
|
637e3f0666 | ||
|
|
ced24ccabb | ||
|
|
fb3e1f75e9 | ||
|
|
0e24009fe9 | ||
|
|
cd24fd8e28 | ||
|
|
dd300c1269 | ||
|
|
44c9e7e664 | ||
|
|
268aa06179 | ||
|
|
a7d83ee416 | ||
|
|
a89f51f9ec | ||
|
|
d2295828d0 | ||
|
|
dc16643e0d | ||
|
|
ac25e89ebf | ||
|
|
819cc1ab21 | ||
|
|
78a95f1ca4 | ||
|
|
5b77345b03 | ||
|
|
23ce7b351d | ||
|
|
375ece06d2 | ||
|
|
4ffd994549 | ||
|
|
58aaa17e7e | ||
|
|
7a05be436c | ||
|
|
3cf084cb96 | ||
|
|
1e59dee685 | ||
|
|
42e423470c | ||
|
|
555f1ae516 | ||
|
|
1c690c5ff8 | ||
|
|
802bdf7dc5 | ||
|
|
1731e00ebd | ||
|
|
7b7c7f57be | ||
|
|
6e728cf812 | ||
|
|
808e8b1c5a | ||
|
|
9201250f74 | ||
|
|
5b28a05eb7 | ||
|
|
3fca309f2c | ||
|
|
d6df0a53b5 | ||
|
|
4b9907fb54 | ||
|
|
ab4e11ae4d | ||
|
|
546fcc16de | ||
|
|
0f4f669cf0 | ||
|
|
2bdd72dfff | ||
|
|
64770a771f | ||
|
|
ebb2c38a0a | ||
|
|
616127cedc | ||
|
|
3129e60a73 | ||
|
|
df724b4006 | ||
|
|
ffdbdb3d02 | ||
|
|
568fde1ce5 | ||
|
|
46945415c9 | ||
|
|
195bd2199c | ||
|
|
0bf844018c | ||
|
|
2319724bb2 | ||
|
|
222d0501df | ||
|
|
cb404b53b5 | ||
|
|
3abc5a5b42 | ||
|
|
6f738df4a5 | ||
|
|
d22b314701 | ||
|
|
786c713ff5 | ||
|
|
d474883e90 | ||
|
|
0d98cbd657 | ||
|
|
15c4edba1a | ||
|
|
e96cfdbbe7 | ||
|
|
3c1efd33e2 | ||
|
|
9c8321f2e0 | ||
|
|
b5c670ebf8 | ||
|
|
95ef061711 | ||
|
|
32b7b771cc | ||
|
|
9efee7bf99 | ||
|
|
8bb5c15745 | ||
|
|
43a58bdba1 | ||
|
|
43e9943757 | ||
|
|
8b588f5313 | ||
|
|
a2e8b47c57 | ||
|
|
83b73ce78e | ||
|
|
782a6318f3 | ||
|
|
a948fd7e10 | ||
|
|
287df8a715 | ||
|
|
dc67630b64 | ||
|
|
e5cdad554e | ||
|
|
b9f5ba0702 | ||
|
|
1b4487e6c9 | ||
|
|
b10d792687 | ||
|
|
5a41c79d7d | ||
|
|
f5b0004a52 | ||
|
|
c063329e9a | ||
|
|
eeb9cbafe7 | ||
|
|
0f38da068c | ||
|
|
7e191eb18b | ||
|
|
a91168fd36 | ||
|
|
4a11cf455f | ||
|
|
adb2382aa5 | ||
|
|
6d6922efa6 | ||
|
|
846fb3072a | ||
|
|
3acd42f8d7 | ||
|
|
c98441b13c | ||
|
|
5cb3e1ded5 | ||
|
|
2b11f408fd | ||
|
|
d0e05e8be2 | ||
|
|
9a3b54b9a3 | ||
|
|
16133212fc | ||
|
|
16df81ac94 | ||
|
|
7d486c2ec6 | ||
|
|
4cb7d21a8f | ||
|
|
476900ab41 | ||
|
|
e6fbba3f80 | ||
|
|
0cea700dd8 | ||
|
|
d5058b3b20 | ||
|
|
355fd7aa53 | ||
|
|
33468630e6 | ||
|
|
48c1c50796 | ||
|
|
a0330a3fb2 | ||
|
|
6ea331ce3b | ||
|
|
a4b792e24d | ||
|
|
c4e2db32b5 | ||
|
|
cbd1bccbf9 | ||
|
|
136688997c | ||
|
|
03ec6d0eee | ||
|
|
30c3aa4f5b | ||
|
|
7ac3ffcc1b | ||
|
|
e23f56e81c | ||
|
|
3ee5501257 | ||
|
|
f5dbd44ebe | ||
|
|
3bfae84d32 | ||
|
|
c1d725ed34 | ||
|
|
de5a00e807 | ||
|
|
f445ac7521 | ||
|
|
9602c149ca | ||
|
|
cf2c18c32b | ||
|
|
887495f38f | ||
|
|
159bf03d49 | ||
|
|
288a8574c3 | ||
|
|
2e701ee8a2 | ||
|
|
eacf1be066 | ||
|
|
3652f1dcb2 | ||
|
|
d7f429d246 | ||
|
|
89e93d90b3 | ||
|
|
f8d983b523 | ||
|
|
7c30579900 | ||
|
|
e4d7a77348 | ||
|
|
83b94ab864 | ||
|
|
3c1bcdb1e2 | ||
|
|
f47ac8f96e | ||
|
|
ed961c795e | ||
|
|
e7ee745488 | ||
|
|
9ae7196a50 | ||
|
|
25a7e85c1c | ||
|
|
eb37f14923 |
@@ -25,8 +25,6 @@ groups:
|
|||||||
name: ENHANCEMENTS
|
name: ENHANCEMENTS
|
||||||
labels:
|
labels:
|
||||||
- kind/enhancement
|
- kind/enhancement
|
||||||
- kind/refactor
|
|
||||||
- kind/ui
|
|
||||||
-
|
-
|
||||||
name: SECURITY
|
name: SECURITY
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
20
.devcontainer/devcontainer.json
Normal file
20
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "Tea DevContainer",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:2.1-trixie",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {},
|
||||||
|
"extensions": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"golang.go",
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
"DavidAnson.vscode-markdownlint",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"GitHub.vscode-pull-request-github"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Dockerfile
|
||||||
|
tea
|
||||||
196
.drone.yml
196
.drone.yml
@@ -1,196 +0,0 @@
|
|||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
name: default
|
|
||||||
|
|
||||||
platform:
|
|
||||||
os: linux
|
|
||||||
arch: amd64
|
|
||||||
|
|
||||||
workspace:
|
|
||||||
base: /go
|
|
||||||
path: src/code.gitea.io/tea
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: build
|
|
||||||
pull: always
|
|
||||||
image: golang:1.13
|
|
||||||
environment:
|
|
||||||
GOPROXY: https://goproxy.cn
|
|
||||||
commands:
|
|
||||||
- make clean
|
|
||||||
- make vet
|
|
||||||
- make lint
|
|
||||||
- make fmt-check
|
|
||||||
- make misspell-check
|
|
||||||
- make test-vendor
|
|
||||||
- make build
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- tag
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
- name: unit-test
|
|
||||||
pull: always
|
|
||||||
image: golang:1.13
|
|
||||||
commands:
|
|
||||||
- make unit-test-coverage
|
|
||||||
settings:
|
|
||||||
group: test
|
|
||||||
when:
|
|
||||||
branch:
|
|
||||||
- master
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
- name: release-test
|
|
||||||
pull: always
|
|
||||||
image: golang:1.13
|
|
||||||
commands:
|
|
||||||
- make test
|
|
||||||
settings:
|
|
||||||
group: test
|
|
||||||
when:
|
|
||||||
branch:
|
|
||||||
- "release/*"
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
- name: tag-test
|
|
||||||
pull: always
|
|
||||||
image: golang:1.13
|
|
||||||
commands:
|
|
||||||
- make test
|
|
||||||
settings:
|
|
||||||
group: test
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- tag
|
|
||||||
|
|
||||||
- name: static
|
|
||||||
pull: always
|
|
||||||
image: techknowlogick/xgo:latest
|
|
||||||
environment:
|
|
||||||
GOPROXY: https://goproxy.cn
|
|
||||||
commands:
|
|
||||||
- export PATH=$PATH:$GOPATH/bin
|
|
||||||
- make release
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- tag
|
|
||||||
|
|
||||||
- name: gpg-sign
|
|
||||||
pull: always
|
|
||||||
image: plugins/gpgsign:1
|
|
||||||
settings:
|
|
||||||
detach_sign: true
|
|
||||||
excludes:
|
|
||||||
- "dist/release/*.sha256"
|
|
||||||
files:
|
|
||||||
- "dist/release/*"
|
|
||||||
environment:
|
|
||||||
GPGSIGN_KEY:
|
|
||||||
from_secret: gpgsign_key
|
|
||||||
GPGSIGN_PASSPHRASE:
|
|
||||||
from_secret: gpgsign_passphrase
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- tag
|
|
||||||
|
|
||||||
- name: tag-release
|
|
||||||
pull: always
|
|
||||||
image: plugins/s3:1
|
|
||||||
settings:
|
|
||||||
acl: public-read
|
|
||||||
bucket: releases
|
|
||||||
endpoint: https://storage.gitea.io
|
|
||||||
path_style: true
|
|
||||||
source: "dist/release/*"
|
|
||||||
strip_prefix: dist/release/
|
|
||||||
target: "/tea/${DRONE_TAG##v}"
|
|
||||||
environment:
|
|
||||||
AWS_ACCESS_KEY_ID:
|
|
||||||
from_secret: aws_access_key_id
|
|
||||||
AWS_SECRET_ACCESS_KEY:
|
|
||||||
from_secret: aws_secret_access_key
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- tag
|
|
||||||
|
|
||||||
- name: release-branch-release
|
|
||||||
pull: always
|
|
||||||
image: plugins/s3:1
|
|
||||||
settings:
|
|
||||||
acl: public-read
|
|
||||||
bucket: releases
|
|
||||||
endpoint: https://storage.gitea.io
|
|
||||||
path_style: true
|
|
||||||
source: "dist/release/*"
|
|
||||||
strip_prefix: dist/release/
|
|
||||||
target: "/tea/${DRONE_BRANCH##release/v}"
|
|
||||||
environment:
|
|
||||||
AWS_ACCESS_KEY_ID:
|
|
||||||
from_secret: aws_access_key_id
|
|
||||||
AWS_SECRET_ACCESS_KEY:
|
|
||||||
from_secret: aws_secret_access_key
|
|
||||||
when:
|
|
||||||
branch:
|
|
||||||
- "release/*"
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
|
|
||||||
- name: release
|
|
||||||
pull: always
|
|
||||||
image: plugins/s3:1
|
|
||||||
settings:
|
|
||||||
acl: public-read
|
|
||||||
bucket: releases
|
|
||||||
endpoint: https://storage.gitea.io
|
|
||||||
path_style: true
|
|
||||||
source: "dist/release/*"
|
|
||||||
strip_prefix: dist/release/
|
|
||||||
target: /tea/master
|
|
||||||
environment:
|
|
||||||
AWS_ACCESS_KEY_ID:
|
|
||||||
from_secret: aws_access_key_id
|
|
||||||
AWS_SECRET_ACCESS_KEY:
|
|
||||||
from_secret: aws_secret_access_key
|
|
||||||
when:
|
|
||||||
branch:
|
|
||||||
- master
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
|
|
||||||
- name: gitea
|
|
||||||
pull: always
|
|
||||||
image: plugins/gitea-release:1
|
|
||||||
settings:
|
|
||||||
files:
|
|
||||||
- "dist/release/*"
|
|
||||||
base_url: https://gitea.com
|
|
||||||
api_key:
|
|
||||||
from_secret: gitea_token
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- tag
|
|
||||||
|
|
||||||
- name: discord
|
|
||||||
pull: always
|
|
||||||
image: appleboy/drone-discord:1.0.0
|
|
||||||
environment:
|
|
||||||
DISCORD_WEBHOOK_ID:
|
|
||||||
from_secret: discord_webhook_id
|
|
||||||
DISCORD_WEBHOOK_TOKEN:
|
|
||||||
from_secret: discord_webhook_token
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- tag
|
|
||||||
- pull_request
|
|
||||||
status:
|
|
||||||
- changed
|
|
||||||
- failure
|
|
||||||
30
.gitea/issue_template/bug.md
Normal file
30
.gitea/issue_template/bug.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: "Bug Report"
|
||||||
|
about: "Use this template when reporting a bug, so you don't forget important information we'd ask for later."
|
||||||
|
title: "Bug: "
|
||||||
|
labels:
|
||||||
|
- kind/bug
|
||||||
|
---
|
||||||
|
|
||||||
|
### describe your environment
|
||||||
|
- tea version used (`tea -v`):
|
||||||
|
- [ ] I also reproduced the issue [with the latest main build](https://dl.gitea.com/tea/main/)
|
||||||
|
- Gitea version used:
|
||||||
|
- [ ] the issue only occurred after updating gitea recently
|
||||||
|
- operating system:
|
||||||
|
- I make use of...
|
||||||
|
- [ ] non-standard default branch names (no `main`,`master`, or `trunk`)
|
||||||
|
- [ ] .ssh/config or .gitconfig host aliases in my git remotes
|
||||||
|
- [ ] ssh_agent or similar
|
||||||
|
- [ ] non-standard ports for gitea and/or ssh
|
||||||
|
- [ ] something else that's likely to interact badly with tea: ...
|
||||||
|
|
||||||
|
|
||||||
|
Please provide the output of `git remote -v` (if the issue is related to tea not finding resources on Gitea):
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### describe the issue (observed vs expected behaviour)
|
||||||
|
|
||||||
|
|
||||||
80
.gitea/workflows/release-nightly.yml
Normal file
80
.gitea/workflows/release-nightly.yml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
- name: import gpg
|
||||||
|
id: import_gpg
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v7
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
|
- name: get SDK version
|
||||||
|
id: sdk_version
|
||||||
|
run: echo "version=$(go list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
distribution: goreleaser-pro
|
||||||
|
version: "~> v1"
|
||||||
|
args: release --nightly
|
||||||
|
env:
|
||||||
|
SDK_VERSION: ${{ steps.sdk_version.outputs.version }}
|
||||||
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
|
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||||
|
GORELEASER_FORCE_TOKEN: 'gitea'
|
||||||
|
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: gitea
|
||||||
|
DOCKER_LATEST: nightly
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
env:
|
||||||
|
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea/tea:latest
|
||||||
85
.gitea/workflows/release-tag.yml
Normal file
85
.gitea/workflows/release-tag.yml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
- name: import gpg
|
||||||
|
id: import_gpg
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v7
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
|
- name: get SDK version
|
||||||
|
id: sdk_version
|
||||||
|
run: echo "version=$(go list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
distribution: goreleaser-pro
|
||||||
|
version: "~> v1"
|
||||||
|
args: release
|
||||||
|
env:
|
||||||
|
SDK_VERSION: ${{ steps.sdk_version.outputs.version }}
|
||||||
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
|
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||||
|
GORELEASER_FORCE_TOKEN: 'gitea'
|
||||||
|
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: gitea
|
||||||
|
DOCKER_LATEST: nightly
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get tag version without v
|
||||||
|
id: get_version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
env:
|
||||||
|
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea/tea:${{ env.VERSION }}
|
||||||
63
.gitea/workflows/test-pr.yml
Normal file
63
.gitea/workflows/test-pr.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: check-and-test
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
#govulncheck_job:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# name: Run govulncheck
|
||||||
|
# steps:
|
||||||
|
# - id: govulncheck
|
||||||
|
# uses: golang/govulncheck-action@v1
|
||||||
|
# with:
|
||||||
|
# go-version-file: 'go.mod'
|
||||||
|
check-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
HTTP_PROXY: ""
|
||||||
|
GITEA_TEA_TEST_URL: "http://gitea:3000"
|
||||||
|
GITEA_TEA_TEST_USERNAME: "test01"
|
||||||
|
GITEA_TEA_TEST_PASSWORD: "test01"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
- name: lint and build
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
make vet
|
||||||
|
make lint
|
||||||
|
make fmt-check
|
||||||
|
make docs-check
|
||||||
|
make build
|
||||||
|
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance
|
||||||
|
- name: test and coverage
|
||||||
|
run: |
|
||||||
|
make test
|
||||||
|
make unit-test-coverage
|
||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
image: docker.gitea.com/gitea:1.25.5
|
||||||
|
cmd:
|
||||||
|
- bash
|
||||||
|
- -c
|
||||||
|
- >-
|
||||||
|
mkdir -p /tmp/conf/
|
||||||
|
&& mkdir -p /tmp/data/
|
||||||
|
&& echo "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT = true" > /tmp/conf/app.ini
|
||||||
|
&& echo "[security]" >> /tmp/conf/app.ini
|
||||||
|
&& echo "INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1NTg4MzY4ODB9.LoKQyK5TN_0kMJFVHWUW0uDAyoGjDP6Mkup4ps2VJN4" >> /tmp/conf/app.ini
|
||||||
|
&& echo "INSTALL_LOCK = true" >> /tmp/conf/app.ini
|
||||||
|
&& echo "SECRET_KEY = 2crAW4UANgvLipDS6U5obRcFosjSJHQANll6MNfX7P0G3se3fKcCwwK3szPyGcbo" >> /tmp/conf/app.ini
|
||||||
|
&& echo "PASSWORD_COMPLEXITY = off" >> /tmp/conf/app.ini
|
||||||
|
&& echo "[database]" >> /tmp/conf/app.ini
|
||||||
|
&& echo "DB_TYPE = sqlite3" >> /tmp/conf/app.ini
|
||||||
|
&& echo "[repository]" >> /tmp/conf/app.ini
|
||||||
|
&& echo "ROOT = /tmp/data/" >> /tmp/conf/app.ini
|
||||||
|
&& echo "[server]" >> /tmp/conf/app.ini
|
||||||
|
&& echo "ROOT_URL = http://gitea:3000" >> /tmp/conf/app.ini
|
||||||
|
&& gitea migrate -c /tmp/conf/app.ini
|
||||||
|
&& gitea admin user create --username=test01 --password=test01 --email=test01@gitea.io --admin=true --must-change-password=false --access-token -c /tmp/conf/app.ini
|
||||||
|
&& gitea web -c /tmp/conf/app.ini
|
||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,6 +1,19 @@
|
|||||||
tea
|
/tea
|
||||||
/gitea-vet
|
/gitea-vet
|
||||||
|
/gitea-vet.exe
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.history/
|
.history/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Nix-specific
|
||||||
|
.direnv/
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
|||||||
45
.golangci.yml
Normal file
45
.golangci.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- govet
|
||||||
|
- revive
|
||||||
|
- misspell
|
||||||
|
- ineffassign
|
||||||
|
- unused
|
||||||
|
|
||||||
|
settings:
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: blank-imports
|
||||||
|
- name: context-as-argument
|
||||||
|
- name: context-keys-type
|
||||||
|
- name: dot-imports
|
||||||
|
- name: error-return
|
||||||
|
- name: error-strings
|
||||||
|
- name: error-naming
|
||||||
|
- name: exported
|
||||||
|
- name: if-return
|
||||||
|
- name: increment-decrement
|
||||||
|
- name: var-declaration
|
||||||
|
- name: range
|
||||||
|
- name: receiver-naming
|
||||||
|
- name: time-naming
|
||||||
|
- name: unexported-return
|
||||||
|
- name: indent-error-flow
|
||||||
|
- name: errorf
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
ignore-words:
|
||||||
|
- unknwon
|
||||||
|
- destory
|
||||||
|
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
12
.goreleaser.checksum.sh
Normal file
12
.goreleaser.checksum.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "usage: $0 <path>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUM=$(shasum -a 256 "$1" | cut -d' ' -f1)
|
||||||
|
BASENAME=$(basename "$1")
|
||||||
|
echo -n "${SUM} ${BASENAME}" > "$1".sha256
|
||||||
122
.goreleaser.yaml
Normal file
122
.goreleaser.yaml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- freebsd
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
goarm:
|
||||||
|
- "5"
|
||||||
|
- "6"
|
||||||
|
- "7"
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm
|
||||||
|
- goos: darwin
|
||||||
|
goarch: ppc64le
|
||||||
|
- goos: darwin
|
||||||
|
goarch: s390x
|
||||||
|
- goos: windows
|
||||||
|
goarch: ppc64le
|
||||||
|
- goos: windows
|
||||||
|
goarch: s390x
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
|
goarm: "5"
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
|
goarm: "6"
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
|
goarm: "7"
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: ppc64le
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: s390x
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
goarm: "5"
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
goarm: "6"
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
goarm: "7"
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm64
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X "code.gitea.io/tea/modules/version.Version={{ trimprefix .Summary "v" }}" -X "code.gitea.io/tea/modules/version.Tags=" -X "code.gitea.io/tea/modules/version.SDK={{ .Env.SDK_VERSION }}"
|
||||||
|
binary: >-
|
||||||
|
{{ .ProjectName }}-
|
||||||
|
{{- .Version }}-
|
||||||
|
{{- .Os }}-
|
||||||
|
{{- if eq .Arch "amd64" }}amd64
|
||||||
|
{{- else if eq .Arch "amd64_v1" }}amd64
|
||||||
|
{{- else if eq .Arch "386" }}386
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
{{- if .Arm }}-{{ .Arm }}{{ end }}
|
||||||
|
no_unique_dist_dir: true
|
||||||
|
hooks:
|
||||||
|
post:
|
||||||
|
- cmd: xz -k -9 {{ .Path }}
|
||||||
|
dir: ./dist/
|
||||||
|
- cmd: sh .goreleaser.checksum.sh {{ .Path }}
|
||||||
|
- cmd: sh .goreleaser.checksum.sh {{ .Path }}.xz
|
||||||
|
|
||||||
|
blobs:
|
||||||
|
-
|
||||||
|
provider: s3
|
||||||
|
bucket: "{{ .Env.S3_BUCKET }}"
|
||||||
|
region: "{{ .Env.S3_REGION }}"
|
||||||
|
folder: "tea/{{.Version}}"
|
||||||
|
extra_files:
|
||||||
|
- glob: ./**.xz
|
||||||
|
- glob: ./**.sha256
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- format: binary
|
||||||
|
name_template: "{{ .Binary }}"
|
||||||
|
allow_different_binary_count: true
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
extra_files:
|
||||||
|
- glob: ./**.xz
|
||||||
|
|
||||||
|
force_token: gitea
|
||||||
|
|
||||||
|
signs:
|
||||||
|
-
|
||||||
|
signature: "${artifact}.sig"
|
||||||
|
artifacts: checksum
|
||||||
|
stdin: '{{ .Env.GPGSIGN_PASSPHRASE }}'
|
||||||
|
args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"]
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Branch }}-devel"
|
||||||
|
|
||||||
|
nightly:
|
||||||
|
name_template: "{{ .Branch }}"
|
||||||
|
|
||||||
|
gitea_urls:
|
||||||
|
api: https://gitea.com/api/v1
|
||||||
|
download: https://gitea.com
|
||||||
|
|
||||||
|
release:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./**.xz
|
||||||
|
- glob: ./**.xz.sha256
|
||||||
|
|
||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||||
|
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||||
@@ -16,7 +16,6 @@ warningCode = 1
|
|||||||
[rule.increment-decrement]
|
[rule.increment-decrement]
|
||||||
[rule.var-naming]
|
[rule.var-naming]
|
||||||
[rule.var-declaration]
|
[rule.var-declaration]
|
||||||
[rule.package-comments]
|
|
||||||
[rule.range]
|
[rule.range]
|
||||||
[rule.receiver-naming]
|
[rule.receiver-naming]
|
||||||
[rule.time-naming]
|
[rule.time-naming]
|
||||||
|
|||||||
182
CHANGELOG.md
182
CHANGELOG.md
@@ -1,5 +1,187 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [v0.13.0](https://gitea.com/gitea/tea/releases/tag/v0.13.0) - 2026-04-05
|
||||||
|
|
||||||
|
* FEATURES
|
||||||
|
* Add `tea pr edit` subcommand for pull requests (#944)
|
||||||
|
* Add `tea repo edit` subcommand (#928)
|
||||||
|
* Support owner-based repository listing in `tea repo ls` (#931)
|
||||||
|
* Store OAuth tokens in OS keyring via credstore (#926)
|
||||||
|
* Support parsing multiple values in `tea api` subcommand (#911)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Replace log.Fatal/os.Exit with proper error returns (#941)
|
||||||
|
* Update to charm libraries v2 (#923)
|
||||||
|
* MISC
|
||||||
|
* Bump Go version to 1.26
|
||||||
|
* Update dependencies: go-git/v5 v5.17.2, gitea SDK v0.24.1, urfave/cli/v3 v3.8.0, oauth2 v0.36.0, tablewriter v1.1.4, go-authgate/sdk-go v0.6.1
|
||||||
|
|
||||||
|
## [v0.9.1](https://gitea.com/gitea/tea/releases/tag/v0.9.1) - 2023-02-15
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Print pull dont crash if it has TeamReviewRequests (#517)
|
||||||
|
|
||||||
|
## [v0.9.0](https://gitea.com/gitea/tea/releases/tag/v0.9.0) - 2022-09-13
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* Rename master branch to main (#495)
|
||||||
|
* Return RFC3339 UTC timestamps for machine-readable output (#470)
|
||||||
|
* FEATURES
|
||||||
|
* Allow editing multiline prompts with external text editor (#429)
|
||||||
|
* Add `tea admin user list` (#427)
|
||||||
|
* Add `tea whoami` command (#426)
|
||||||
|
* Add `tea org create <name>` (#420)
|
||||||
|
* Add `tea clone` (#411)
|
||||||
|
* Add `tea repo fork` (#410)
|
||||||
|
* Add `tea repo create-from-template` (#408)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fetch all items where needed. (#475)
|
||||||
|
* Fix running in repos without remote (#472)
|
||||||
|
* Add TSV to machine-readable formats (#467)
|
||||||
|
* Fix create milestone with deadline bug (#462)
|
||||||
|
* Fix resolving of URLs in markdown (#401)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Don't emit ANSI sequences when not emitting to TTY for markdown (#491)
|
||||||
|
* Show more version info (#486)
|
||||||
|
* Add preference `flag_defaults.remote`, refactor (#466)
|
||||||
|
* Add `--fields` to notification & milestone listings (#422)
|
||||||
|
* PR listing: add --fields & expose additional fields (#415)
|
||||||
|
* Add more flags to `tea repo create` (#409)
|
||||||
|
* Implement more issue filters (#400)
|
||||||
|
* MISC
|
||||||
|
* Simplify build & update installation instructions (#437)
|
||||||
|
* Clarify command descriptions when no arguments are taken (#496)
|
||||||
|
* Improve Documentation (#433)
|
||||||
|
* Use golang v1.18 and drop vendor folder (#478)
|
||||||
|
* Correct spelling of "wether" to "whether" in usage output (#453)
|
||||||
|
|
||||||
|
## [v0.8.0](https://gitea.com/gitea/tea/releases/tag/v0.8.0) - 2021-09-22
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* `tea notifications --all` has moved to `tea notifications --mine` (#389)
|
||||||
|
* `tea notifications` now only works with the context of a remote repo. (#389)
|
||||||
|
To run this outside of a local git dir, run either tea n `--mine` or `tea n --repo <my/repo>`
|
||||||
|
* FEATURES
|
||||||
|
* Add `tea pr merge` (#348)
|
||||||
|
* BUGFIXES
|
||||||
|
* Don't skip reading the local repo when `--repo` specifies a repo slug (#398)
|
||||||
|
* Fix adding login without token on private instances (#392)
|
||||||
|
* Correctly match login by ssh host with port (#391)
|
||||||
|
* Fix printing issue deadline (#388)
|
||||||
|
* Return useful error on wrong sshkey path (#374)
|
||||||
|
* Fix parsing of `--description` for issue/pr create (#371)
|
||||||
|
* Add missing flags (#369)
|
||||||
|
* Check negative limit command parameter (#358) (#359)
|
||||||
|
* Add missing flags to org & labels subcommands (#357)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Don't require a body for comment PR reviews (#399)
|
||||||
|
* Accept more main branch names for login detection (#396)
|
||||||
|
* Make local repo optional for `tea pr create`(#393)
|
||||||
|
* Notifications Add State Field (#384)
|
||||||
|
* Improve error messages (#370)
|
||||||
|
* Add tab completion for fish shell (#364)
|
||||||
|
* Text editor selection: follow unix defacto standards (#356)
|
||||||
|
* MISC
|
||||||
|
* Update Dependencies (#390)
|
||||||
|
|
||||||
|
## [v0.7.1](https://gitea.com/gitea/tea/releases/tag/v0.7.1) - 2021-08-27
|
||||||
|
|
||||||
|
* BUILD
|
||||||
|
* Enable release builds for darwin/arm64 (#360)
|
||||||
|
|
||||||
|
## [v0.7.0](https://gitea.com/gitea/tea/releases/tag/v0.7.0) - 2021-03-12
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* `tea issue create`: move `-b` flag to `-d` (#331)
|
||||||
|
* Drop `tea notif` shorthand in favor of `tea n` (#307)
|
||||||
|
* FEATURES
|
||||||
|
* Add commands for reviews (#315)
|
||||||
|
* Add `tea comment` and show comments of issues/pulls (#313)
|
||||||
|
* Add interactive mode for `tea milestone create` (#310)
|
||||||
|
* Add command to install shell completion (#309)
|
||||||
|
* Implement PR closing and reopening (#304)
|
||||||
|
* Add interactive mode for `tea issue create` (#302)
|
||||||
|
* BUGFIXES
|
||||||
|
* Introduce workaround for missing pull head sha (#340)
|
||||||
|
* Don't exit if we can't find a local repo with a remote matching to a login (#336)
|
||||||
|
* Don't push before creating a pull (#334)
|
||||||
|
* InitCommand() robustness (#327)
|
||||||
|
* `tea comment`: handle piped stdin (#322)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Allow checking out PRs with deleted head branch (#341)
|
||||||
|
* Markdown renderer: detect terminal width, resolve relative URLs (#332)
|
||||||
|
* Add more issue / pr creation parameters (#331)
|
||||||
|
* Improve `tea time` (#319)
|
||||||
|
* `tea pr checkout`: dont create local branches (#314)
|
||||||
|
* Add `tea issues --fields`, allow printing labels (#312)
|
||||||
|
* Add more command shorthands (#307)
|
||||||
|
* Show PR CI status (#306)
|
||||||
|
* Make PR workflow helpers more robust (#300)
|
||||||
|
|
||||||
|
## [v0.6.0](https://gitea.com/gitea/tea/releases/tag/v0.6.0) - 2020-12-11
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* Add `tea repos search`, improve repo listing (#215)
|
||||||
|
* Add Detail View for Login (#212)
|
||||||
|
* FEATURES
|
||||||
|
* Add interactive mode for `tea pr create` (#279)
|
||||||
|
* Add organization delete command (#270)
|
||||||
|
* Add organization list command (#264)
|
||||||
|
* BUGFIXES
|
||||||
|
* Forces needed arguments to `tea ms issues` (#297)
|
||||||
|
* Subcommands work outside of git repos (#285)
|
||||||
|
* Fix repo flag ignores local repo for login detection (#285)
|
||||||
|
* Improve ssh handling (#277)
|
||||||
|
* Issue create return web url (#257)
|
||||||
|
* Support prerelease gitea instances (#252)
|
||||||
|
* Fix `tea pr create` within same repo (#248)
|
||||||
|
* Handle login name case-insensitive on all comands (#227)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Add `tea login delete` (#296)
|
||||||
|
* Release delete: add --delete-tag & --confirm (#286)
|
||||||
|
* Sorted milestones list (#281)
|
||||||
|
* Pull clean & checkout use token for http(s) auth (#275)
|
||||||
|
* Show more infos in pull detail view (#271)
|
||||||
|
* Specify fields to print on `tea repos list` (#223)
|
||||||
|
* Print times in local timezone (#217)
|
||||||
|
* Issue create/edit print details (#214)
|
||||||
|
* Improve `tea logout` (#213)
|
||||||
|
* Added a shorthand for notifications (#209)
|
||||||
|
* Common subcommand naming scheme (#208)
|
||||||
|
* `tea pr checkout`: fetch via ssh if available (#192)
|
||||||
|
* Major refactor of codebase
|
||||||
|
* BUILD
|
||||||
|
* Use gox to cross-compile (#274)
|
||||||
|
* DOCS
|
||||||
|
* Update Docs to new code structure (#247)
|
||||||
|
|
||||||
|
## [v0.5.0](https://gitea.com/gitea/tea/releases/tag/v0.5.0) - 2020-09-27
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* Add Login Manage Functions (#182)
|
||||||
|
* FEATURES
|
||||||
|
* Add Release Subcomands (#195)
|
||||||
|
* Render Markdown and colorize labels table (#181)
|
||||||
|
* Add BasicAuth & Interactive for Login (#174)
|
||||||
|
* Add milestones subcomands (#149)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix Pulls Create (#202)
|
||||||
|
* Pulls create: detect head branch repo owner (#193)
|
||||||
|
* Fix Labels Delete (#180)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Add Pagination Options for List Subcomands (#204)
|
||||||
|
* Issues/Pulls: Details show State (#196)
|
||||||
|
* Make issues & pulls subcommands consistent (#188)
|
||||||
|
* Update SDK to v0.13.0 (#179)
|
||||||
|
* More Options To Specify Repo (#178)
|
||||||
|
* Add Repo Create subcomand & enhancements (#173)
|
||||||
|
* Times: format duration as seconds for machine-readable outputs (#168)
|
||||||
|
* Add user message to login list view (#166)
|
||||||
|
|
||||||
|
## [v0.4.1](https://gitea.com/gitea/tea/releases/tag/v0.4.1) - 2020-09-13
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Notification don't relay on a repo (#159)
|
||||||
|
|
||||||
## [v0.4.0](https://gitea.com/gitea/tea/pulls?q=&type=all&state=closed&milestone=1264) - 2020-07-18
|
## [v0.4.0](https://gitea.com/gitea/tea/pulls?q=&type=all&state=closed&milestone=1264) - 2020-07-18
|
||||||
|
|
||||||
* FEATURES
|
* FEATURES
|
||||||
|
|||||||
153
CONTRIBUTING.md
153
CONTRIBUTING.md
@@ -7,8 +7,6 @@
|
|||||||
- [Bug reports](#bug-reports)
|
- [Bug reports](#bug-reports)
|
||||||
- [Discuss your design](#discuss-your-design)
|
- [Discuss your design](#discuss-your-design)
|
||||||
- [Testing redux](#testing-redux)
|
- [Testing redux](#testing-redux)
|
||||||
- [Vendoring](#vendoring)
|
|
||||||
- [Translation](#translation)
|
|
||||||
- [Code review](#code-review)
|
- [Code review](#code-review)
|
||||||
- [Styleguide](#styleguide)
|
- [Styleguide](#styleguide)
|
||||||
- [Sign-off your work](#sign-off-your-work)
|
- [Sign-off your work](#sign-off-your-work)
|
||||||
@@ -20,36 +18,31 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
This document explains how to contribute changes to the Gitea project.
|
This document explains how to contribute changes to TEA.
|
||||||
It assumes you have followed the
|
|
||||||
[installation instructions](https://docs.gitea.io/en-us/).
|
|
||||||
Sensitive security-related issues should be reported to
|
Sensitive security-related issues should be reported to
|
||||||
[security@gitea.io](mailto:security@gitea.io).
|
[security@gitea.io](mailto:security@gitea.io).
|
||||||
|
|
||||||
For configuring IDE or code editor to develop Gitea see [IDE and code editor configuration](contrib/ide/)
|
For configuring IDE or code editor to develop Gitea see [IDE and code editor configuration](https://github.com/go-gitea/gitea/tree/master/contrib/ide)
|
||||||
|
|
||||||
## Bug reports
|
## Bug reports
|
||||||
|
|
||||||
Please search the issues on the issue tracker with a variety of keywords
|
Please search the issues on the issue tracker with a variety of keywords
|
||||||
to ensure your bug is not already reported.
|
to ensure your bug is not already reported.
|
||||||
|
|
||||||
If unique, [open an issue](https://github.com/go-gitea/gitea/issues/new)
|
If unique, [open an issue](https://gitea.com/gitea/tea/issues/new).
|
||||||
and answer the questions so we can understand and reproduce the
|
|
||||||
problematic behavior.
|
|
||||||
|
|
||||||
To show us that the issue you are having is in Gitea itself, please
|
Please write clear, concise instructions so we can reproduce the behavior—
|
||||||
write clear, concise instructions so we can reproduce the behavior—
|
|
||||||
even if it seems obvious. The more detailed and specific you are,
|
even if it seems obvious. The more detailed and specific you are,
|
||||||
the faster we can fix the issue. Check out [How to Report Bugs
|
the faster we can fix the issue. Check out [How to Report Bugs
|
||||||
Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||||
|
|
||||||
Please be kind, remember that Gitea comes at no cost to you, and you're
|
Please be kind, remember that TEA comes at no cost to you, and you're
|
||||||
getting free help.
|
getting free help.
|
||||||
|
|
||||||
## Discuss your design
|
## Discuss your design
|
||||||
|
|
||||||
The project welcomes submissions. If you want to change or add something,
|
The project welcomes submissions. If you want to change or add something,
|
||||||
please let everyone know what you're working on—[file an issue](https://github.com/go-gitea/gitea/issues/new)!
|
please let everyone know what you're working on—[file an issue](https://gitea.com/gitea/tea/issues/new)!
|
||||||
Significant changes must go through the change proposal process
|
Significant changes must go through the change proposal process
|
||||||
before they can be accepted. To create a proposal, file an issue with
|
before they can be accepted. To create a proposal, file an issue with
|
||||||
your proposed changes documented, and make sure to note in the title
|
your proposed changes documented, and make sure to note in the title
|
||||||
@@ -63,55 +56,14 @@ high-level discussions.
|
|||||||
|
|
||||||
## Testing redux
|
## Testing redux
|
||||||
|
|
||||||
Before sending code out for review, run all the tests for the
|
Before sending code out for review, run all the test by executing: `make test`
|
||||||
whole tree to make sure the changes don't break other usage
|
Since TEA is an cli tool it should be obvious to test your feature locally first.
|
||||||
and keep the compatibility on upgrade. To make sure you are
|
|
||||||
running the test suite exactly like we do, you should install
|
|
||||||
the CLI for [Drone CI](https://github.com/drone/drone), as
|
|
||||||
we are using the server for continous testing, following [these
|
|
||||||
instructions](http://docs.drone.io/cli-installation/). After that,
|
|
||||||
you can simply call `drone exec --local --build-event "pull_request"` within
|
|
||||||
your working directory and it will try to run the test suite locally.
|
|
||||||
|
|
||||||
## Vendoring
|
|
||||||
|
|
||||||
We keep a cached copy of dependencies within the `vendor/` directory,
|
|
||||||
managing updates via [dep](https://github.com/golang/dep).
|
|
||||||
|
|
||||||
Pull requests should only include `vendor/` updates if they are part of
|
|
||||||
the same change, be it a bugfix or a feature addition.
|
|
||||||
|
|
||||||
The `vendor/` update needs to be justified as part of the PR description,
|
|
||||||
and must be verified by the reviewers and/or merger to always reference
|
|
||||||
an existing upstream commit.
|
|
||||||
|
|
||||||
You can find more information on how to get started with it on the [dep project website](https://golang.github.io/dep/docs/introduction.html).
|
|
||||||
|
|
||||||
## Translation
|
|
||||||
|
|
||||||
We do all translation work inside [Crowdin](https://crowdin.com/project/gitea).
|
|
||||||
The only translation that is maintained in this git repository is
|
|
||||||
[`en_US.ini`](https://github.com/go-gitea/gitea/blob/master/options/locale/locale_en-US.ini)
|
|
||||||
and is synced regularily to Crowdin. Once a translation has reached
|
|
||||||
A SATISFACTORY PERCENTAGE it will be synced back into this repo and
|
|
||||||
included in the next released version.
|
|
||||||
|
|
||||||
## Building Gitea
|
|
||||||
|
|
||||||
Generally, the go build tools are installed as-needed in the `Makefile`.
|
|
||||||
An exception are the tools to build the CSS and images.
|
|
||||||
|
|
||||||
- To build CSS: Install [Node.js](https://nodejs.org/en/download/package-manager)
|
|
||||||
with `npm` and then run `npm install` and `make generate-stylesheets`.
|
|
||||||
- To build Images: ImageMagick, inkscape and zopflipng binaries must be
|
|
||||||
available in your `PATH` to run `make generate-images`.
|
|
||||||
|
|
||||||
## Code review
|
## Code review
|
||||||
|
|
||||||
Changes to Gitea must be reviewed before they are accepted—no matter who
|
Changes to TEA must be reviewed before they are accepted—no matter who
|
||||||
makes the change, even if they are an owner or a maintainer. We use GitHub's
|
makes the change, even if they are an owner or a maintainer. We use Gitea's
|
||||||
pull request workflow to do that. And, we also use [LGTM](http://lgtm.co)
|
pull request & review workflow to do that. Gitea ensure every PR is reviewed by at least 2 maintainers.
|
||||||
to ensure every PR is reviewed by at least 2 maintainers.
|
|
||||||
|
|
||||||
Please try to make your pull request easy to review for us. And, please read
|
Please try to make your pull request easy to review for us. And, please read
|
||||||
the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)* guide;
|
the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)* guide;
|
||||||
@@ -128,6 +80,41 @@ Some of the key points:
|
|||||||
|
|
||||||
## Styleguide
|
## Styleguide
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
- Subcommands should follow the following structure:
|
||||||
|
```
|
||||||
|
tea <noun> <verb> [<noun>] [<flags>]
|
||||||
|
```
|
||||||
|
|
||||||
|
for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
tea issues list
|
||||||
|
tea pulls create
|
||||||
|
tea teams add user --team x --user y
|
||||||
|
```
|
||||||
|
- Commands should accept nouns as singular & plural by making use of the `Aliases` field.
|
||||||
|
- The default action without a verb is `list`.
|
||||||
|
- There is a standard set of verbs: `list/ls`, `create`, `edit`, `delete`
|
||||||
|
- `ls` lists objects with filter options, and applies pagination where available.
|
||||||
|
- `delete` should show info what is deleted and ask user again, if force flag`-y` is not set
|
||||||
|
- Verbs that accept large numbers of flags provide an interactive mode when called without any arguments or flags.
|
||||||
|
- Try to reuse as many flag definitions as possible, see `cmd/flags/flags.go`.
|
||||||
|
- Always make sure that the help texts are properly set, and as concise as possible.
|
||||||
|
|
||||||
|
### Internal Module Structure
|
||||||
|
- `cmd`: only contains command/flag options for `urfave/cli`
|
||||||
|
- subcommands are in a subpackage named after its parent command
|
||||||
|
- `modules/task`: contain func for doing something with gitea
|
||||||
|
(e.g. create token by user/passwd)
|
||||||
|
- `modules/print`: contain all functions that print to stdout
|
||||||
|
- `modules/config`: config tea & login things
|
||||||
|
- `modules/interact`: contain functions to interact with user by prompts
|
||||||
|
- `modules/git`: do git related stuff (get info/push/pull/checkout/...)
|
||||||
|
- `modules/utils`: helper functions used by other functions
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
Use `make fmt`, check with `make lint`.
|
||||||
For imports you should use the following format (_without_ the comments)
|
For imports you should use the following format (_without_ the comments)
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
@@ -164,25 +151,7 @@ commit automatically with `git commit -s`.
|
|||||||
|
|
||||||
## Release Cycle
|
## Release Cycle
|
||||||
|
|
||||||
We adopted a release schedule to streamline the process of working
|
Before we reach v1 there is no fixed release cycle.
|
||||||
on, finishing, and issuing releases. The overall goal is to make a
|
|
||||||
minor release every two months, which breaks down into one month of
|
|
||||||
general development followed by one month of testing and polishing
|
|
||||||
known as the release freeze. All the feature pull requests should be
|
|
||||||
merged in the first month of one release period. And, during the frozen
|
|
||||||
period, a corresponding release branch is open for fixes backported from
|
|
||||||
master. Release candidates are made during this period for user testing to
|
|
||||||
obtain a final version that is maintained in this branch. A release is
|
|
||||||
maintained by issuing patch releases to only correct critical problems
|
|
||||||
such as crashes or security issues.
|
|
||||||
|
|
||||||
Major release cycles are bimonthly. They always begin on the 25th and end on
|
|
||||||
the 24th (i.e., the 25th of December to February 24th).
|
|
||||||
|
|
||||||
During a development cycle, we may also publish any necessary minor releases
|
|
||||||
for the previous version. For example, if the latest, published release is
|
|
||||||
v1.2, then minor changes for the previous release—e.g., v1.1.0 -> v1.1.1—are
|
|
||||||
still possible.
|
|
||||||
|
|
||||||
## Maintainers
|
## Maintainers
|
||||||
|
|
||||||
@@ -191,7 +160,7 @@ maintainers](MAINTAINERS). Every PR **MUST** be reviewed by at least
|
|||||||
two maintainers (or owners) before it can get merged. A maintainer
|
two maintainers (or owners) before it can get merged. A maintainer
|
||||||
should be a contributor of Gitea (or Gogs) and contributed at least
|
should be a contributor of Gitea (or Gogs) and contributed at least
|
||||||
4 accepted PRs. A contributor should apply as a maintainer in the
|
4 accepted PRs. A contributor should apply as a maintainer in the
|
||||||
[Discord](https://discord.gg/NsatcWJ) #develop channel. The owners
|
[Discord](https://discord.gg/Gitea) #develop channel. The owners
|
||||||
or the team maintainers may invite the contributor. A maintainer
|
or the team maintainers may invite the contributor. A maintainer
|
||||||
should spend some time on code reviews. If a maintainer has no
|
should spend some time on code reviews. If a maintainer has no
|
||||||
time to do that, they should apply to leave the maintainers team
|
time to do that, they should apply to leave the maintainers team
|
||||||
@@ -208,6 +177,9 @@ https://help.github.com/articles/signing-commits-with-gpg/
|
|||||||
|
|
||||||
## Owners
|
## Owners
|
||||||
|
|
||||||
|
This repo is part of the Gitea project and as such part of that project's
|
||||||
|
governance.
|
||||||
|
|
||||||
Since Gitea is a pure community organization without any company support,
|
Since Gitea is a pure community organization without any company support,
|
||||||
to keep the development healthy we will elect three owners every year. All
|
to keep the development healthy we will elect three owners every year. All
|
||||||
contributors may vote to elect up to three candidates, one of which will
|
contributors may vote to elect up to three candidates, one of which will
|
||||||
@@ -221,7 +193,7 @@ https://help.github.com/articles/securing-your-account-with-two-factor-authentic
|
|||||||
|
|
||||||
After the election, the new owners should proactively agree
|
After the election, the new owners should proactively agree
|
||||||
with our [CONTRIBUTING](CONTRIBUTING.md) requirements in the
|
with our [CONTRIBUTING](CONTRIBUTING.md) requirements in the
|
||||||
[Discord](https://discord.gg/NsatcWJ) #general channel. Below are the
|
[Discord](https://discord.gg/Gitea) #general channel. Below are the
|
||||||
words to speak:
|
words to speak:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -230,22 +202,9 @@ I'm honored to having been elected an owner of Gitea, I agree with
|
|||||||
and lead the development of Gitea.
|
and lead the development of Gitea.
|
||||||
```
|
```
|
||||||
|
|
||||||
To honor the past owners, here's the history of the owners and the time
|
|
||||||
they served:
|
|
||||||
|
|
||||||
* 2016-11-04 ~ 2017-12-31
|
|
||||||
* [Lunny Xiao](https://github.com/lunny) <xiaolunwen@gmail.com>
|
|
||||||
* [Thomas Boerger](https://github.com/tboerger) <thomas@webhippie.de>
|
|
||||||
* [Kim Carlbäcker](https://github.com/bkcsoft) <kim.carlbacker@gmail.com>
|
|
||||||
|
|
||||||
* 2018-01-01 ~ 2018-12-31
|
|
||||||
* [Lunny Xiao](https://github.com/lunny) <xiaolunwen@gmail.com>
|
|
||||||
* [Lauris Bukšis-Haberkorns](https://github.com/lafriks) <lauris@nix.lv>
|
|
||||||
* [Kim Carlbäcker](https://github.com/bkcsoft) <kim.carlbacker@gmail.com>
|
|
||||||
|
|
||||||
## Versions
|
## Versions
|
||||||
|
|
||||||
Gitea has the `master` branch as a tip branch and has version branches
|
tea has the `master` branch as a tip branch and has version branches
|
||||||
such as `release/v0.9`. `release/v0.9` is a release branch and we will
|
such as `release/v0.9`. `release/v0.9` is a release branch and we will
|
||||||
tag `v0.9.0` for binary download. If `v0.9.0` has bugs, we will accept
|
tag `v0.9.0` for binary download. If `v0.9.0` has bugs, we will accept
|
||||||
pull requests on the `release/v0.9` branch and publish a `v0.9.1` tag,
|
pull requests on the `release/v0.9` branch and publish a `v0.9.1` tag,
|
||||||
@@ -261,9 +220,9 @@ be reviewed by two maintainers and must pass the automatic tests.
|
|||||||
Code that you contribute should use the standard copyright header:
|
Code that you contribute should use the standard copyright header:
|
||||||
|
|
||||||
```
|
```
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// SPDX-License-Identifier: MIT
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Files in the repository contain copyright from the year they are added
|
Files in the repository contain copyright from the year they are added
|
||||||
|
|||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM docker.io/chainguard/go:latest AS build
|
||||||
|
COPY . /build/
|
||||||
|
WORKDIR /build
|
||||||
|
RUN make build && mkdir -p /app/.config/tea
|
||||||
|
|
||||||
|
FROM docker.io/chainguard/busybox:latest-glibc
|
||||||
|
COPY --from=build /build/tea /bin/tea
|
||||||
|
COPY --from=build --chown=65532:65532 /app /app
|
||||||
|
VOLUME [ "/app" ]
|
||||||
|
ENV HOME="/app"
|
||||||
|
ENTRYPOINT ["/bin/sh", "-c"]
|
||||||
|
CMD [ "tea" ]
|
||||||
1
LICENSE
1
LICENSE
@@ -1,5 +1,4 @@
|
|||||||
Copyright (c) 2016 The Gitea Authors
|
Copyright (c) 2016 The Gitea Authors
|
||||||
Copyright (c) 2015 The Gogs Authors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
173
Makefile
173
Makefile
@@ -1,30 +1,14 @@
|
|||||||
DIST := dist
|
DIST := dist
|
||||||
IMPORT := code.gitea.io/tea
|
|
||||||
export GO111MODULE=on
|
|
||||||
|
|
||||||
GO ?= go
|
GO ?= go
|
||||||
SED_INPLACE := sed -i
|
|
||||||
SHASUM ?= shasum -a 256
|
SHASUM ?= shasum -a 256
|
||||||
|
|
||||||
export PATH := $($(GO) env GOPATH)/bin:$(PATH)
|
export PATH := $($(GO) env GOPATH)/bin:$(PATH)
|
||||||
|
|
||||||
ifeq ($(OS), Windows_NT)
|
GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
|
||||||
EXECUTABLE := tea.exe
|
|
||||||
else
|
|
||||||
EXECUTABLE := tea
|
|
||||||
UNAME_S := $(shell uname -s)
|
|
||||||
ifeq ($(UNAME_S),Darwin)
|
|
||||||
SED_INPLACE := sed -i ''
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
GOFILES := $(shell find . -name "*.go" -type f ! -path "./vendor/*" ! -path "*/bindata.go")
|
# Tool packages with pinned versions
|
||||||
GOFMT ?= gofmt -s
|
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
|
||||||
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
||||||
GOFLAGS := -i -v
|
|
||||||
EXTRA_GOFLAGS ?=
|
|
||||||
|
|
||||||
MAKE_VERSION := $(shell make -v | head -n 1)
|
|
||||||
|
|
||||||
ifneq ($(DRONE_TAG),)
|
ifneq ($(DRONE_TAG),)
|
||||||
VERSION ?= $(subst v,,$(DRONE_TAG))
|
VERSION ?= $(subst v,,$(DRONE_TAG))
|
||||||
@@ -33,160 +17,109 @@ else
|
|||||||
ifneq ($(DRONE_BRANCH),)
|
ifneq ($(DRONE_BRANCH),)
|
||||||
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
|
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
|
||||||
else
|
else
|
||||||
VERSION ?= master
|
VERSION ?= main
|
||||||
endif
|
endif
|
||||||
TEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
|
TEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
|
||||||
endif
|
endif
|
||||||
|
TEA_VERSION_TAG ?= $(shell sed 's/+/_/' <<< $(TEA_VERSION))
|
||||||
LDFLAGS := -X "main.Version=$(TEA_VERSION)" -X "main.Tags=$(TAGS)"
|
|
||||||
|
|
||||||
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)
|
|
||||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
|
||||||
|
|
||||||
TAGS ?=
|
TAGS ?=
|
||||||
|
SDK ?= $(shell $(GO) list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)
|
||||||
|
LDFLAGS := -X "code.gitea.io/tea/modules/version.Version=$(TEA_VERSION)" -X "code.gitea.io/tea/modules/version.Tags=$(TAGS)" -X "code.gitea.io/tea/modules/version.SDK=$(SDK)" -s -w
|
||||||
|
|
||||||
|
# override to allow passing additional goflags via make CLI
|
||||||
|
override GOFLAGS := $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)'
|
||||||
|
|
||||||
|
PACKAGES ?= $(shell $(GO) list ./...)
|
||||||
|
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||||
|
|
||||||
|
# OS specific vars.
|
||||||
ifeq ($(OS), Windows_NT)
|
ifeq ($(OS), Windows_NT)
|
||||||
EXECUTABLE := tea.exe
|
EXECUTABLE := tea.exe
|
||||||
|
VET_TOOL := gitea-vet.exe
|
||||||
else
|
else
|
||||||
EXECUTABLE := tea
|
EXECUTABLE := tea
|
||||||
|
VET_TOOL := gitea-vet
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# $(call strip-suffix,filename)
|
|
||||||
strip-suffix = $(firstword $(subst ., ,$(1)))
|
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
$(GO) clean -mod=vendor -i ./...
|
$(GO) clean -i ./...
|
||||||
rm -rf $(EXECUTABLE) $(DIST)
|
rm -rf $(EXECUTABLE) $(DIST)
|
||||||
|
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt:
|
fmt:
|
||||||
$(GOFMT) -w $(GOFILES)
|
$(GO) run $(GOFUMPT_PACKAGE) -w $(GOFILES)
|
||||||
|
|
||||||
.PHONY: vet
|
.PHONY: vet
|
||||||
vet:
|
vet:
|
||||||
# Default vet
|
# Default vet
|
||||||
$(GO) vet -mod=vendor $(PACKAGES)
|
$(GO) vet $(PACKAGES)
|
||||||
# Custom vet
|
# Custom vet
|
||||||
$(GO) build -mod=vendor gitea.com/jolheiser/gitea-vet
|
$(GO) build code.gitea.io/gitea-vet
|
||||||
$(GO) vet -vettool=gitea-vet $(PACKAGES)
|
$(GO) vet -vettool=$(VET_TOOL) $(PACKAGES)
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint:
|
lint:
|
||||||
@hash revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
|
||||||
cd /tmp && $(GO) get -u github.com/mgechev/revive; \
|
|
||||||
fi
|
|
||||||
revive -config .revive.toml -exclude=./vendor/... ./... || exit 1
|
|
||||||
|
|
||||||
.PHONY: misspell-check
|
.PHONY: lint-fix
|
||||||
misspell-check:
|
lint-fix:
|
||||||
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
||||||
cd /tmp && $(GO) get -u github.com/client9/misspell/cmd/misspell; \
|
|
||||||
fi
|
|
||||||
misspell -error -i unknwon,destory $(GOFILES)
|
|
||||||
|
|
||||||
.PHONY: misspell
|
|
||||||
misspell:
|
|
||||||
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
|
||||||
cd /tmp && $(GO) get -u github.com/client9/misspell/cmd/misspell; \
|
|
||||||
fi
|
|
||||||
misspell -w -i unknwon $(GOFILES)
|
|
||||||
|
|
||||||
.PHONY: fmt-check
|
.PHONY: fmt-check
|
||||||
fmt-check:
|
fmt-check:
|
||||||
# get all go files and run go fmt on them
|
# get all go files and run gofumpt on them
|
||||||
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
@diff=$$($(GO) run $(GOFUMPT_PACKAGE) -d $(GOFILES)); \
|
||||||
if [ -n "$$diff" ]; then \
|
if [ -n "$$diff" ]; then \
|
||||||
echo "Please run 'make fmt' and commit the result:"; \
|
echo "Please run 'make fmt' and commit the result:"; \
|
||||||
echo "$${diff}"; \
|
echo "$${diff}"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
|
.PHONY: docs
|
||||||
|
docs:
|
||||||
|
$(GO) run docs/docs.go --out docs/CLI.md
|
||||||
|
|
||||||
|
.PHONY: docs-check
|
||||||
|
docs-check:
|
||||||
|
@DIFF=$$($(GO) run docs/docs.go | diff docs/CLI.md -); \
|
||||||
|
if [ -n "$$DIFF" ]; then \
|
||||||
|
echo "Please run 'make docs' and commit the result:"; \
|
||||||
|
echo "$$DIFF"; \
|
||||||
|
exit 1; \
|
||||||
|
fi;
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
$(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
|
$(GO) test -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
|
||||||
|
|
||||||
.PHONY: unit-test-coverage
|
.PHONY: unit-test-coverage
|
||||||
unit-test-coverage:
|
unit-test-coverage:
|
||||||
$(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' -cover -coverprofile coverage.out $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
$(GO) test -tags='sqlite sqlite_unlock_notify' -cover -coverprofile coverage.out $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||||
|
|
||||||
.PHONY: vendor
|
.PHONY: tidy
|
||||||
vendor:
|
tidy:
|
||||||
$(GO) mod tidy && $(GO) mod vendor
|
$(GO) mod tidy
|
||||||
|
|
||||||
.PHONY: test-vendor
|
|
||||||
test-vendor: vendor
|
|
||||||
@diff=$$(git diff vendor/); \
|
|
||||||
if [ -n "$$diff" ]; then \
|
|
||||||
echo "Please run 'make vendor' and commit the result:"; \
|
|
||||||
echo "$${diff}"; \
|
|
||||||
exit 1; \
|
|
||||||
fi;
|
|
||||||
|
|
||||||
.PHONY: check
|
.PHONY: check
|
||||||
check: test
|
check: test
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: $(wildcard *.go)
|
install: $(SOURCES)
|
||||||
$(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
|
@echo "installing to $(shell $(GO) env GOPATH)/bin/$(EXECUTABLE)"
|
||||||
|
$(GO) install -v $(BUILDMODE) $(GOFLAGS)
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: $(EXECUTABLE)
|
build: $(EXECUTABLE)
|
||||||
|
|
||||||
$(EXECUTABLE): $(SOURCES)
|
$(EXECUTABLE): $(SOURCES)
|
||||||
$(GO) build -mod=vendor $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
|
$(GO) build $(BUILDMODE) $(GOFLAGS) -o $@
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: build-image
|
||||||
release: release-dirs release-windows release-linux release-darwin release-copy release-compress release-check
|
build-image:
|
||||||
|
docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) .
|
||||||
|
|
||||||
.PHONY: release-dirs
|
|
||||||
release-dirs:
|
|
||||||
mkdir -p $(DIST)/binaries $(DIST)/release
|
|
||||||
|
|
||||||
.PHONY: release-windows
|
|
||||||
release-windows:
|
|
||||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
|
||||||
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
|
|
||||||
fi
|
|
||||||
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out tea-$(VERSION) .
|
|
||||||
ifeq ($(CI),drone)
|
|
||||||
cp /build/* $(DIST)/binaries
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: release-linux
|
|
||||||
release-linux:
|
|
||||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
|
||||||
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
|
|
||||||
fi
|
|
||||||
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out tea-$(VERSION) .
|
|
||||||
ifeq ($(CI),drone)
|
|
||||||
cp /build/* $(DIST)/binaries
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: release-darwin
|
|
||||||
release-darwin:
|
|
||||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
|
||||||
cd /tmp && $(GO) get -u src.techknowlogick.com/xgo; \
|
|
||||||
fi
|
|
||||||
GO111MODULE=off xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out tea-$(VERSION) .
|
|
||||||
ifeq ($(CI),drone)
|
|
||||||
cp /build/* $(DIST)/binaries
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: release-copy
|
|
||||||
release-copy:
|
|
||||||
cd $(DIST); for file in `find /build -type f -name "*"`; do cp $${file} ./release/; done;
|
|
||||||
|
|
||||||
.PHONY: release-compress
|
|
||||||
release-compress:
|
|
||||||
@hash gxz > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
|
||||||
GO111MODULE=off $(GO) get -u github.com/ulikunitz/xz/cmd/gxz; \
|
|
||||||
fi
|
|
||||||
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && gxz -k -9 $${file}; done;
|
|
||||||
|
|
||||||
.PHONY: release-check
|
|
||||||
release-check:
|
|
||||||
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done;
|
|
||||||
|
|||||||
229
README.md
229
README.md
@@ -1,84 +1,203 @@
|
|||||||
# <img alt='' src='https://gitea.com/repo-avatars/550-80a3a8c2ab0e2c2d69f296b7f8582485' height="40"/> *T E A*
|
# <img alt='tea logo' src='https://gitea.com/repo-avatars/550-80a3a8c2ab0e2c2d69f296b7f8582485' height="40"/> *T E A*
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT) [](https://gitea.com/gitea/tea/releases) [](https://drone.gitea.com/gitea/tea) [](https://discord.gg/Gitea) [](https://goreportcard.com/report/code.gitea.io/tea) [](https://godoc.org/code.gitea.io/tea)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://gitea.com/gitea/tea/releases)
|
||||||
|
[](https://discord.gg/Gitea)
|
||||||
|
[](https://goreportcard.com/report/code.gitea.io/tea) [](https://godoc.org/code.gitea.io/tea)
|
||||||
|

|
||||||
|
|
||||||
## The official CLI interface for gitea
|
## The official CLI for Gitea
|
||||||
|
|
||||||
Tea is a command line tool for interacting on one or more Gitea instances.
|

|
||||||
It uses [code.gitea.io/sdk](https://code.gitea.io/sdk) and interacts with the Gitea API
|
|
||||||
|
|
||||||

|
```
|
||||||
|
NAME:
|
||||||
|
tea - command line tool to interact with Gitea
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
tea [global options] [command [command options]]
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
Version: 0.10.1+15-g8876fe3 golang: 1.25.0 go-sdk: v0.21.0
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
tea is a productivity helper for Gitea. It can be used to manage most entities on
|
||||||
|
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
|
||||||
|
|
||||||
|
tea tries to make use of context provided by the repository in $PWD if available.
|
||||||
|
tea works best in a upstream/fork workflow, when the local main branch tracks the
|
||||||
|
upstream repo. tea assumes that local git state is published on the remote before
|
||||||
|
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
help, h Shows a list of commands or help for one command
|
||||||
|
|
||||||
|
ENTITIES:
|
||||||
|
issues, issue, i List, create and update issues
|
||||||
|
pulls, pull, pr Manage and checkout pull requests
|
||||||
|
labels, label Manage issue labels
|
||||||
|
milestones, milestone, ms List and create milestones
|
||||||
|
releases, release, r Manage releases
|
||||||
|
times, time, t Operate on tracked times of a repository's issues & pulls
|
||||||
|
organizations, organization, org List, create, delete organizations
|
||||||
|
repos, repo Show repository details
|
||||||
|
branches, branch, b Consult branches
|
||||||
|
actions Manage repository actions (secrets, variables)
|
||||||
|
comment, c Add a comment to an issue / pr
|
||||||
|
webhooks, webhook Manage repository webhooks
|
||||||
|
|
||||||
|
HELPERS:
|
||||||
|
open, o Open something of the repository in web browser
|
||||||
|
notifications, notification, n Show notifications
|
||||||
|
clone, C Clone a repository locally
|
||||||
|
|
||||||
|
MISCELLANEOUS:
|
||||||
|
whoami Show current logged in user
|
||||||
|
admin, a Operations requiring admin access on the Gitea instance
|
||||||
|
|
||||||
|
SETUP:
|
||||||
|
logins, login Log in to a Gitea server
|
||||||
|
logout Log out from a Gitea server
|
||||||
|
|
||||||
|
GLOBAL OPTIONS:
|
||||||
|
--debug, --vvv Enable debug mode (default: false)
|
||||||
|
--help, -h show help
|
||||||
|
--version, -v print the version
|
||||||
|
|
||||||
|
EXAMPLES
|
||||||
|
tea login add # add a login once to get started
|
||||||
|
|
||||||
|
tea pulls # list open pulls for the repo in $PWD
|
||||||
|
tea pulls --repo $HOME/foo # list open pulls for the repo in $HOME/foo
|
||||||
|
tea pulls --remote upstream # list open pulls for the repo pointed at by
|
||||||
|
# your local "upstream" git remote
|
||||||
|
# list open pulls for any gitea repo at the given login instance
|
||||||
|
tea pulls --repo gitea/tea --login gitea.com
|
||||||
|
|
||||||
|
tea milestone issues 0.7.0 # view open issues for milestone '0.7.0'
|
||||||
|
tea issue 189 # view contents of issue 189
|
||||||
|
tea open 189 # open web ui for issue 189
|
||||||
|
tea open milestones # open web ui for milestones
|
||||||
|
|
||||||
|
tea actions secrets list # list all repository action secrets
|
||||||
|
tea actions secrets create API_KEY # create a new secret (will prompt for value)
|
||||||
|
tea actions variables list # list all repository action variables
|
||||||
|
tea actions variables set API_URL https://api.example.com
|
||||||
|
|
||||||
|
tea webhooks list # list repository webhooks
|
||||||
|
tea webhooks list --org myorg # list organization webhooks
|
||||||
|
tea webhooks create https://example.com/hook --events push,pull_request
|
||||||
|
|
||||||
|
# send gitea desktop notifications every 5 minutes (bash + libnotify)
|
||||||
|
while :; do tea notifications --mine -o simple | xargs -i notify-send {}; sleep 300; done
|
||||||
|
|
||||||
|
ABOUT
|
||||||
|
Written & maintained by The Gitea Authors.
|
||||||
|
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea.
|
||||||
|
More info about Gitea itself on https://about.gitea.com.
|
||||||
|
```
|
||||||
|
|
||||||
|
- tea uses [code.gitea.io/sdk](https://code.gitea.io/sdk) and interacts with the Gitea API.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
You can use the prebuilt binaries from [dl.gitea.io](https://dl.gitea.io/tea/)
|
There are different ways to get `tea`:
|
||||||
|
|
||||||
|
1. Install via your system package manager:
|
||||||
|
- macOS via `brew` (official):
|
||||||
|
```sh
|
||||||
|
brew install tea
|
||||||
|
```
|
||||||
|
- arch linux ([tea](https://archlinux.org/packages/extra/x86_64/tea/), thirdparty)
|
||||||
|
- alpine linux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge), thirdparty)
|
||||||
|
- Windows via `MSYS2` ([tea](https://packages.msys2.org/base/mingw-w64-tea), thirdparty)
|
||||||
|
|
||||||
To install from source, go 1.12 or newer is required:
|
2. Use the prebuilt binaries from [dl.gitea.com](https://dl.gitea.com/tea/)
|
||||||
```sh
|
|
||||||
go get code.gitea.io/tea
|
3. Install from source: [see *Compilation*](#compilation)
|
||||||
go install code.gitea.io/tea
|
|
||||||
|
4. Docker: [Tea at docker hub](https://hub.docker.com/r/gitea/tea)
|
||||||
|
|
||||||
|
### Log in to Gitea from tea
|
||||||
|
|
||||||
|
Gitea can use many different authentication schemes, and not every authentication method will work with every Gitea deployment. When you are a Gitea instance administrator you can tweak your settings to fit your use case. For the method that is most likely to work with any Gitea deployment use the following steps:
|
||||||
|
|
||||||
|
1. Open your Gitea instance in a web browser
|
||||||
|
|
||||||
|
2. Log in to Gitea in your web browser. Any MFA, IDP, or whatever else should be available this way.
|
||||||
|
|
||||||
|
3. In your "user settings", generate an application token with at least **user read** permissions. If you want to do anything useful with the token add additional permissions/scopes.
|
||||||
|
|
||||||
|
4. Run `tea login add`, select **application token** authentication when asked for authentication type, and answer **yes** to the question if you have a token. Paste the generated token when asked for one.
|
||||||
|
|
||||||
|
You should now be logged in to your gitea instance from tea.
|
||||||
|
|
||||||
|
Since 0.10 Gitea supports the much simpler oauth workflow but oauth may not be available on all Gitea deployments, and gets much more complex when running tea on a remote system.
|
||||||
|
|
||||||
|
### Shell completion
|
||||||
|
|
||||||
|
If you installed from source or the package does not provide the completions with it you can add them yourself with `tea completion <shell>` command which is not visible in help. To generate the completions run one of the following commands depending on your shell.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# .bashrc
|
||||||
|
source <(tea completion bash)
|
||||||
|
|
||||||
|
# .zshrc
|
||||||
|
source <(tea completion zsh)
|
||||||
|
|
||||||
|
# fish
|
||||||
|
tea completion fish > ~/.config/fish/completions/tea.fish
|
||||||
|
|
||||||
|
# Powershell
|
||||||
|
Output the script to path/to/autocomplete/tea.ps1 an run it.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Man Page
|
||||||
|
|
||||||
If you have `brew` installed, you can install `tea` via:
|
The hidden command `tea man` can be used to generate the `tea` man page.
|
||||||
|
|
||||||
```sh
|
```shell
|
||||||
brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea
|
# for bash or zsh
|
||||||
brew install tea
|
man <(tea man)
|
||||||
|
|
||||||
|
# for fish
|
||||||
|
man (tea man | psub)
|
||||||
|
|
||||||
|
# write man page to a file
|
||||||
|
tea man --out ./tea.man
|
||||||
```
|
```
|
||||||
|
|
||||||
Distribution packages exist for: **alpinelinux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge))** and **archlinux ([gitea-tea](https://aur.archlinux.org/packages/gitea-tea))**
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
First of all, you have to create a token on your `personal settings -> application` page of your gitea instance.
|
|
||||||
Use this token to login with `tea`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tea login add --name=try --url=https://try.gitea.io --token=xxxxxx
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you can use the following `tea` subcommands.
|
|
||||||
Detailed usage information is available via `tea <command> --help`.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
login Log in to a Gitea server
|
|
||||||
logout Log out from a Gitea server
|
|
||||||
issues List, create and update issues
|
|
||||||
pulls List, checkout and clean pull requests
|
|
||||||
releases Create releases
|
|
||||||
repos Operate with repositories
|
|
||||||
labels Manage issue labels
|
|
||||||
times Operate on tracked times of a repositorys issues and pulls
|
|
||||||
open Open something of the repository on web browser
|
|
||||||
```
|
|
||||||
|
|
||||||
To fetch issues from different repos, use the `--remote` flag (when inside a gitea repository directory) or `--login` & `--repo` flags.
|
|
||||||
|
|
||||||
## Compilation
|
## Compilation
|
||||||
|
|
||||||
Make sure you have installed a current go version.
|
Make sure you have a current Go version installed (1.26 or newer).
|
||||||
To compile the sources yourself run the following:
|
|
||||||
|
|
||||||
```sh
|
- To compile the source yourself with the recommended flags & tags:
|
||||||
git clone https://gitea.com/gitea/tea.git
|
```sh
|
||||||
cd tea
|
git clone https://gitea.com/gitea/tea.git # or: tea clone gitea.com/gitea/tea ;)
|
||||||
make
|
cd tea
|
||||||
```
|
make
|
||||||
|
```
|
||||||
|
Note that GNU Make (gmake on OpenBSD) is required.
|
||||||
|
If you want to install the compiled program you have to execute the following command:
|
||||||
|
```sh
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
This installs the binary into the "bin" folder inside of your GOPATH folder (`go env GOPATH`). It is possible that this folder isn't in your PATH Environment Variable.
|
||||||
|
|
||||||
|
- For a quick installation without `git` & `make`, set $version and exec:
|
||||||
|
```sh
|
||||||
|
go install code.gitea.io/tea@${version}
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Fork -> Patch -> Push -> Pull Request
|
Fork -> Patch -> Push -> Pull Request
|
||||||
|
|
||||||
- `make test` run testsuite
|
- `make test` run testsuite
|
||||||
- `make vendor` when adding new dependencies
|
- `make vet` run checks (check the order of imports; preventing failure on CI pipeline beforehand)
|
||||||
- ... (for other development tasks, check the `Makefile`)
|
- ... (for other development tasks, check the `Makefile`)
|
||||||
|
|
||||||
## Authors
|
**Please** read the [CONTRIBUTING](CONTRIBUTING.md) documentation, it will tell you about internal structures and concepts.
|
||||||
|
|
||||||
* [Maintainers](https://github.com/orgs/go-gitea/people)
|
|
||||||
* [Contributors](https://github.com/go-gitea/tea/graphs/contributors)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
9
build.go
9
build.go
@@ -1,7 +1,8 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// SPDX-License-Identifier: MIT
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
//+build vendor
|
//go:build vendor
|
||||||
|
// +build vendor
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -10,5 +11,5 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
// for vet
|
// for vet
|
||||||
_ "gitea.com/jolheiser/gitea-vet"
|
_ "code.gitea.io/gitea-vet"
|
||||||
)
|
)
|
||||||
|
|||||||
47
cmd/actions.go
Normal file
47
cmd/actions.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/actions"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdActions represents the actions command for managing Gitea Actions
|
||||||
|
var CmdActions = cli.Command{
|
||||||
|
Name: "actions",
|
||||||
|
Aliases: []string{"action"},
|
||||||
|
Category: catEntities,
|
||||||
|
Usage: "Manage repository actions",
|
||||||
|
Description: "Manage repository actions including secrets, variables, and workflow runs",
|
||||||
|
Action: runActionsDefault,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&actions.CmdActionsSecrets,
|
||||||
|
&actions.CmdActionsVariables,
|
||||||
|
&actions.CmdActionsRuns,
|
||||||
|
&actions.CmdActionsWorkflows,
|
||||||
|
},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "repo",
|
||||||
|
Usage: "repository to operate on",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "login",
|
||||||
|
Usage: "gitea login instance to use",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "output format [table, csv, simple, tsv, yaml, json]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runActionsDefault(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
return cli.ShowSubcommandHelp(cmd)
|
||||||
|
}
|
||||||
31
cmd/actions/runs.go
Normal file
31
cmd/actions/runs.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/actions/runs"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdActionsRuns represents the actions runs command
|
||||||
|
var CmdActionsRuns = cli.Command{
|
||||||
|
Name: "runs",
|
||||||
|
Aliases: []string{"run"},
|
||||||
|
Usage: "Manage workflow runs",
|
||||||
|
Description: "List, view, and manage workflow runs for repository actions",
|
||||||
|
Action: runRunsDefault,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&runs.CmdRunsList,
|
||||||
|
&runs.CmdRunsView,
|
||||||
|
&runs.CmdRunsDelete,
|
||||||
|
&runs.CmdRunsLogs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRunsDefault(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
return runs.RunRunsList(ctx, cmd)
|
||||||
|
}
|
||||||
71
cmd/actions/runs/delete.go
Normal file
71
cmd/actions/runs/delete.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runs
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdRunsDelete represents a sub command to delete/cancel workflow runs
|
||||||
|
var CmdRunsDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"remove", "rm", "cancel"},
|
||||||
|
Usage: "Delete or cancel a workflow run",
|
||||||
|
Description: "Delete (cancel) a workflow run from the repository",
|
||||||
|
ArgsUsage: "<run-id>",
|
||||||
|
Action: runRunsDelete,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Aliases: []string{"y"},
|
||||||
|
Usage: "confirm deletion without prompting",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRunsDelete(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("run ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
runIDStr := cmd.Args().First()
|
||||||
|
runID, err := strconv.ParseInt(runIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid run ID: %s", runIDStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmd.Bool("confirm") {
|
||||||
|
fmt.Printf("Are you sure you want to delete run %d? [y/N] ", runID)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "Y" && response != "yes" {
|
||||||
|
fmt.Println("Deletion canceled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DeleteRepoActionRun(c.Owner, c.Repo, runID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete run: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Run %d deleted successfully\n", runID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
148
cmd/actions/runs/list.go
Normal file
148
cmd/actions/runs/list.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runs
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdRunsList represents a sub command to list workflow runs
|
||||||
|
var CmdRunsList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List workflow runs",
|
||||||
|
Description: "List workflow runs for repository actions with optional filtering",
|
||||||
|
Action: RunRunsList,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "status",
|
||||||
|
Usage: "Filter by status (success, failure, pending, queued, in_progress, skipped, canceled)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "branch",
|
||||||
|
Usage: "Filter by branch name",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "event",
|
||||||
|
Usage: "Filter by event type (push, pull_request, etc.)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "actor",
|
||||||
|
Usage: "Filter by actor username (who triggered the run)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "since",
|
||||||
|
Usage: "Show runs started after this time (e.g., '24h', '2024-01-01')",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "until",
|
||||||
|
Usage: "Show runs started before this time (e.g., '2024-01-01')",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTimeFlag parses time flags like "24h" or "2024-01-01"
|
||||||
|
func parseTimeFlag(value string) (time.Time, error) {
|
||||||
|
if value == "" {
|
||||||
|
return time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as duration (e.g., "24h", "168h")
|
||||||
|
if duration, err := time.ParseDuration(value); err == nil {
|
||||||
|
return time.Now().Add(-duration), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as date
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02 15:04",
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
time.RFC3339,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.Parse(format, value); err == nil {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, fmt.Errorf("unable to parse time: %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunRunsList lists workflow runs
|
||||||
|
func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
// Parse time filters
|
||||||
|
since, err := parseTimeFlag(cmd.String("since"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid --since value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
until, err := parseTimeFlag(cmd.String("until"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid --until value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build list options
|
||||||
|
listOpts := flags.GetListOptions(cmd)
|
||||||
|
|
||||||
|
runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
|
||||||
|
ListOptions: listOpts,
|
||||||
|
Status: cmd.String("status"),
|
||||||
|
Branch: cmd.String("branch"),
|
||||||
|
Event: cmd.String("event"),
|
||||||
|
Actor: cmd.String("actor"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runs == nil {
|
||||||
|
return print.ActionRunsList(nil, c.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by time if specified
|
||||||
|
filteredRuns := filterRunsByTime(runs.WorkflowRuns, since, until)
|
||||||
|
|
||||||
|
return print.ActionRunsList(filteredRuns, c.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterRunsByTime filters runs based on time range
|
||||||
|
func filterRunsByTime(runs []*gitea.ActionWorkflowRun, since, until time.Time) []*gitea.ActionWorkflowRun {
|
||||||
|
if since.IsZero() && until.IsZero() {
|
||||||
|
return runs
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered []*gitea.ActionWorkflowRun
|
||||||
|
for _, run := range runs {
|
||||||
|
if !since.IsZero() && run.StartedAt.Before(since) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !until.IsZero() && run.StartedAt.After(until) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, run)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
111
cmd/actions/runs/list_test.go
Normal file
111
cmd/actions/runs/list_test.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runs
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilterRunsByTime(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
runs := []*gitea.ActionWorkflowRun{
|
||||||
|
{ID: 1, StartedAt: now.Add(-1 * time.Hour)},
|
||||||
|
{ID: 2, StartedAt: now.Add(-2 * time.Hour)},
|
||||||
|
{ID: 3, StartedAt: now.Add(-3 * time.Hour)},
|
||||||
|
{ID: 4, StartedAt: now.Add(-4 * time.Hour)},
|
||||||
|
{ID: 5, StartedAt: now.Add(-5 * time.Hour)},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
since time.Time
|
||||||
|
until time.Time
|
||||||
|
expected []int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no filter",
|
||||||
|
since: time.Time{},
|
||||||
|
until: time.Time{},
|
||||||
|
expected: []int64{1, 2, 3, 4, 5},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "since 2.5 hours ago",
|
||||||
|
since: now.Add(-150 * time.Minute),
|
||||||
|
until: time.Time{},
|
||||||
|
expected: []int64{1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "until 2.5 hours ago",
|
||||||
|
since: time.Time{},
|
||||||
|
until: now.Add(-150 * time.Minute),
|
||||||
|
expected: []int64{3, 4, 5},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "between 2 and 4 hours ago",
|
||||||
|
since: now.Add(-4 * time.Hour),
|
||||||
|
until: now.Add(-2 * time.Hour),
|
||||||
|
expected: []int64{2, 3, 4},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter excludes all",
|
||||||
|
since: now.Add(-30 * time.Minute),
|
||||||
|
until: time.Time{},
|
||||||
|
expected: []int64{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := filterRunsByTime(runs, tt.since, tt.until)
|
||||||
|
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("filterRunsByTime() returned %d runs, want %d", len(result), len(tt.expected))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, run := range result {
|
||||||
|
if run.ID != tt.expected[i] {
|
||||||
|
t.Errorf("filterRunsByTime()[%d].ID = %d, want %d", i, run.ID, tt.expected[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRunsListRequiresRepoContext(t *testing.T) {
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, os.Chdir(t.TempDir()))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, os.Chdir(oldWd))
|
||||||
|
})
|
||||||
|
|
||||||
|
config.SetConfigForTesting(config.LocalConfig{
|
||||||
|
Logins: []config.Login{{
|
||||||
|
Name: "test",
|
||||||
|
URL: "https://gitea.example.com",
|
||||||
|
Token: "token",
|
||||||
|
User: "tester",
|
||||||
|
Default: true,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Name: CmdRunsList.Name,
|
||||||
|
Flags: CmdRunsList.Flags,
|
||||||
|
}
|
||||||
|
require.NoError(t, cmd.Set("login", "test"))
|
||||||
|
|
||||||
|
err = RunRunsList(stdctx.Background(), cmd)
|
||||||
|
require.ErrorContains(t, err, "remote repository required")
|
||||||
|
}
|
||||||
175
cmd/actions/runs/logs.go
Normal file
175
cmd/actions/runs/logs.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runs
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdRunsLogs represents a sub command to view workflow run logs
|
||||||
|
var CmdRunsLogs = cli.Command{
|
||||||
|
Name: "logs",
|
||||||
|
Aliases: []string{"log"},
|
||||||
|
Usage: "View workflow run logs",
|
||||||
|
Description: "View logs for a workflow run or specific job",
|
||||||
|
ArgsUsage: "<run-id>",
|
||||||
|
Action: runRunsLogs,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "job",
|
||||||
|
Usage: "specific job ID to view logs for (if omitted, shows all jobs)",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "follow",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "follow log output (like tail -f), requires job to be in progress",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRunsLogs(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("run ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
runIDStr := cmd.Args().First()
|
||||||
|
runID, err := strconv.ParseInt(runIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid run ID: %s", runIDStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if follow mode is enabled
|
||||||
|
follow := cmd.Bool("follow")
|
||||||
|
|
||||||
|
// If specific job ID provided, fetch only that job's logs
|
||||||
|
jobIDStr := cmd.String("job")
|
||||||
|
if jobIDStr != "" {
|
||||||
|
jobID, err := strconv.ParseInt(jobIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid job ID: %s", jobIDStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if follow {
|
||||||
|
return followJobLogs(client, c, jobID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, _, err := client.GetRepoActionJobLogs(c.Owner, c.Repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get logs for job %d: %w", jobID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Logs for job %d:\n", jobID)
|
||||||
|
fmt.Printf("---\n%s\n", string(logs))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, fetch all jobs and their logs
|
||||||
|
jobs, _, err := client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(jobs.Jobs) == 0 {
|
||||||
|
fmt.Printf("No jobs found for run %d\n", runID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If following and multiple jobs, require --job flag
|
||||||
|
if follow && len(jobs.Jobs) > 1 {
|
||||||
|
return fmt.Errorf("--follow requires --job when run has multiple jobs (found %d jobs)", len(jobs.Jobs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If following with single job, follow it
|
||||||
|
if follow && len(jobs.Jobs) == 1 {
|
||||||
|
return followJobLogs(client, c, jobs.Jobs[0].ID, jobs.Jobs[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch logs for each job
|
||||||
|
for i, job := range jobs.Jobs {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Job: %s (ID: %d)\n", job.Name, job.ID)
|
||||||
|
fmt.Printf("Status: %s\n", job.Status)
|
||||||
|
fmt.Println("---")
|
||||||
|
|
||||||
|
logs, _, err := client.GetRepoActionJobLogs(c.Owner, c.Repo, job.ID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error fetching logs: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(logs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// followJobLogs continuously fetches and displays logs for a running job
|
||||||
|
func followJobLogs(client *gitea.Client, c *context.TeaContext, jobID int64, jobName string) error {
|
||||||
|
var lastLogLength int
|
||||||
|
|
||||||
|
if jobName != "" {
|
||||||
|
fmt.Printf("Following logs for job '%s' (ID: %d) - press Ctrl+C to stop...\n", jobName, jobID)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Following logs for job %d (press Ctrl+C to stop)...\n", jobID)
|
||||||
|
}
|
||||||
|
fmt.Println("---")
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Fetch job status
|
||||||
|
job, _, err := client.GetRepoActionJob(c.Owner, c.Repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if job is still running
|
||||||
|
isRunning := job.Status == "in_progress" || job.Status == "queued" || job.Status == "pending"
|
||||||
|
|
||||||
|
// Fetch logs
|
||||||
|
logs, _, err := client.GetRepoActionJobLogs(c.Owner, c.Repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get logs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display new content only
|
||||||
|
if len(logs) > lastLogLength {
|
||||||
|
newLogs := string(logs)[lastLogLength:]
|
||||||
|
fmt.Print(newLogs)
|
||||||
|
lastLogLength = len(logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If job is complete, exit
|
||||||
|
if !isRunning {
|
||||||
|
fmt.Printf("\n---\nJob completed with status: %s\n", job.Status)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before next poll
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
83
cmd/actions/runs/view.go
Normal file
83
cmd/actions/runs/view.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runs
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdRunsView represents a sub command to view workflow run details
|
||||||
|
var CmdRunsView = cli.Command{
|
||||||
|
Name: "view",
|
||||||
|
Aliases: []string{"show", "get"},
|
||||||
|
Usage: "View workflow run details",
|
||||||
|
Description: "View details of a specific workflow run including jobs",
|
||||||
|
ArgsUsage: "<run-id>",
|
||||||
|
Action: runRunsView,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "jobs",
|
||||||
|
Usage: "show jobs table",
|
||||||
|
Value: true,
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRunsView(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("run ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
runIDStr := cmd.Args().First()
|
||||||
|
runID, err := strconv.ParseInt(runIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid run ID: %s", runIDStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch run details
|
||||||
|
run, _, err := client.GetRepoActionRun(c.Owner, c.Repo, runID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get run: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print run details
|
||||||
|
print.ActionRunDetails(run)
|
||||||
|
|
||||||
|
// Fetch and print jobs if requested
|
||||||
|
if cmd.Bool("jobs") {
|
||||||
|
jobs, _, err := client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobs != nil && len(jobs.Jobs) > 0 {
|
||||||
|
fmt.Printf("\nJobs:\n\n")
|
||||||
|
if err := print.ActionWorkflowJobsList(jobs.Jobs, c.Output); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
30
cmd/actions/secrets.go
Normal file
30
cmd/actions/secrets.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/actions/secrets"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdActionsSecrets represents the actions secrets command
|
||||||
|
var CmdActionsSecrets = cli.Command{
|
||||||
|
Name: "secrets",
|
||||||
|
Aliases: []string{"secret"},
|
||||||
|
Usage: "Manage repository action secrets",
|
||||||
|
Description: "Manage secrets used by repository actions and workflows",
|
||||||
|
Action: runSecretsDefault,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&secrets.CmdSecretsList,
|
||||||
|
&secrets.CmdSecretsCreate,
|
||||||
|
&secrets.CmdSecretsDelete,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSecretsDefault(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
return secrets.RunSecretsList(ctx, cmd)
|
||||||
|
}
|
||||||
74
cmd/actions/secrets/create.go
Normal file
74
cmd/actions/secrets/create.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdSecretsCreate represents a sub command to create action secrets
|
||||||
|
var CmdSecretsCreate = cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Aliases: []string{"add", "set"},
|
||||||
|
Usage: "Create an action secret",
|
||||||
|
Description: "Create a secret for use in repository actions and workflows",
|
||||||
|
ArgsUsage: "<secret-name> [secret-value]",
|
||||||
|
Action: runSecretsCreate,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "file",
|
||||||
|
Usage: "read secret value from file",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "stdin",
|
||||||
|
Usage: "read secret value from stdin",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("secret name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
secretName := cmd.Args().First()
|
||||||
|
|
||||||
|
// Read secret value using the utility
|
||||||
|
secretValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
|
||||||
|
ResourceName: "secret",
|
||||||
|
PromptMsg: fmt.Sprintf("Enter secret value for '%s'", secretName),
|
||||||
|
Hidden: true,
|
||||||
|
AllowEmpty: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.CreateRepoActionSecret(c.Owner, c.Repo, secretName, gitea.CreateOrUpdateSecretOption{
|
||||||
|
Data: secretValue,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Secret '%s' created successfully\n", secretName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
cmd/actions/secrets/create_test.go
Normal file
56
cmd/actions/secrets/create_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSecretSourceArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid args",
|
||||||
|
args: []string{"VALID_SECRET", "secret_value"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing name",
|
||||||
|
args: []string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many args",
|
||||||
|
args: []string{"SECRET_NAME", "value", "extra"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid secret name",
|
||||||
|
args: []string{"invalid_secret", "value"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Test argument validation only
|
||||||
|
if len(tt.args) == 0 {
|
||||||
|
if !tt.wantErr {
|
||||||
|
t.Error("Expected error for empty args")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tt.args) > 2 {
|
||||||
|
if !tt.wantErr {
|
||||||
|
t.Error("Expected error for too many args")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
66
cmd/actions/secrets/delete.go
Normal file
66
cmd/actions/secrets/delete.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdSecretsDelete represents a sub command to delete action secrets
|
||||||
|
var CmdSecretsDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"remove", "rm"},
|
||||||
|
Usage: "Delete an action secret",
|
||||||
|
Description: "Delete a secret used by repository actions",
|
||||||
|
ArgsUsage: "<secret-name>",
|
||||||
|
Action: runSecretsDelete,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Aliases: []string{"y"},
|
||||||
|
Usage: "confirm deletion without prompting",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSecretsDelete(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("secret name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
secretName := cmd.Args().First()
|
||||||
|
|
||||||
|
if !cmd.Bool("confirm") {
|
||||||
|
fmt.Printf("Are you sure you want to delete secret '%s'? [y/N] ", secretName)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "Y" && response != "yes" {
|
||||||
|
fmt.Println("Deletion canceled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DeleteRepoActionSecret(c.Owner, c.Repo, secretName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Secret '%s' deleted successfully\n", secretName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
93
cmd/actions/secrets/delete_test.go
Normal file
93
cmd/actions/secrets/delete_test.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecretsDeleteValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid secret name",
|
||||||
|
args: []string{"VALID_SECRET"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no args",
|
||||||
|
args: []string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many args",
|
||||||
|
args: []string{"SECRET1", "SECRET2"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid secret name but client does not validate",
|
||||||
|
args: []string{"invalid_secret"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateDeleteArgs(tt.args)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateDeleteArgs() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecretsDeleteFlags(t *testing.T) {
|
||||||
|
cmd := CmdSecretsDelete
|
||||||
|
|
||||||
|
// Test command properties
|
||||||
|
if cmd.Name != "delete" {
|
||||||
|
t.Errorf("Expected command name 'delete', got %s", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that rm is one of the aliases
|
||||||
|
hasRmAlias := false
|
||||||
|
for _, alias := range cmd.Aliases {
|
||||||
|
if alias == "rm" {
|
||||||
|
hasRmAlias = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRmAlias {
|
||||||
|
t.Error("Expected 'rm' to be one of the aliases for delete command")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.ArgsUsage != "<secret-name>" {
|
||||||
|
t.Errorf("Expected ArgsUsage '<secret-name>', got %s", cmd.ArgsUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Usage == "" {
|
||||||
|
t.Error("Delete command should have usage text")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("Delete command should have description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateDeleteArgs validates arguments for the delete command
|
||||||
|
func validateDeleteArgs(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("secret name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
return fmt.Errorf("only one secret name allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
49
cmd/actions/secrets/list.go
Normal file
49
cmd/actions/secrets/list.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdSecretsList represents a sub command to list action secrets
|
||||||
|
var CmdSecretsList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List action secrets",
|
||||||
|
Description: "List secrets configured for repository actions",
|
||||||
|
Action: RunSecretsList,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunSecretsList list action secrets
|
||||||
|
func RunSecretsList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
secrets, _, err := client.ListRepoActionSecret(c.Owner, c.Repo, gitea.ListRepoActionSecretOption{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.ActionSecretsList(secrets, c.Output)
|
||||||
|
}
|
||||||
98
cmd/actions/secrets/list_test.go
Normal file
98
cmd/actions/secrets/list_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecretsListFlags(t *testing.T) {
|
||||||
|
cmd := CmdSecretsList
|
||||||
|
|
||||||
|
// Test that required flags exist
|
||||||
|
expectedFlags := []string{"output", "remote", "login", "repo"}
|
||||||
|
|
||||||
|
for _, flagName := range expectedFlags {
|
||||||
|
found := false
|
||||||
|
for _, flag := range cmd.Flags {
|
||||||
|
if flag.Names()[0] == flagName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected flag %s not found in CmdSecretsList", flagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test command properties
|
||||||
|
if cmd.Name != "list" {
|
||||||
|
t.Errorf("Expected command name 'list', got %s", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "ls" {
|
||||||
|
t.Errorf("Expected alias 'ls' for list command")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Usage == "" {
|
||||||
|
t.Error("List command should have usage text")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("List command should have description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecretsListValidation(t *testing.T) {
|
||||||
|
// Basic validation that the command accepts the expected arguments
|
||||||
|
// More detailed testing would require mocking the Gitea client
|
||||||
|
|
||||||
|
// Test that list command doesn't require arguments
|
||||||
|
args := []string{}
|
||||||
|
if len(args) > 0 {
|
||||||
|
t.Error("List command should not require arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that extra arguments are ignored
|
||||||
|
extraArgs := []string{"extra", "args"}
|
||||||
|
if len(extraArgs) > 0 {
|
||||||
|
// This is fine - list commands typically ignore extra args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSecretsListRequiresRepoContext(t *testing.T) {
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, os.Chdir(t.TempDir()))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, os.Chdir(oldWd))
|
||||||
|
})
|
||||||
|
|
||||||
|
config.SetConfigForTesting(config.LocalConfig{
|
||||||
|
Logins: []config.Login{{
|
||||||
|
Name: "test",
|
||||||
|
URL: "https://gitea.example.com",
|
||||||
|
Token: "token",
|
||||||
|
User: "tester",
|
||||||
|
Default: true,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Name: CmdSecretsList.Name,
|
||||||
|
Flags: CmdSecretsList.Flags,
|
||||||
|
}
|
||||||
|
require.NoError(t, cmd.Set("login", "test"))
|
||||||
|
|
||||||
|
err = RunSecretsList(stdctx.Background(), cmd)
|
||||||
|
require.ErrorContains(t, err, "remote repository required")
|
||||||
|
}
|
||||||
30
cmd/actions/variables.go
Normal file
30
cmd/actions/variables.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/actions/variables"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdActionsVariables represents the actions variables command
|
||||||
|
var CmdActionsVariables = cli.Command{
|
||||||
|
Name: "variables",
|
||||||
|
Aliases: []string{"variable", "vars", "var"},
|
||||||
|
Usage: "Manage repository action variables",
|
||||||
|
Description: "Manage variables used by repository actions and workflows",
|
||||||
|
Action: runVariablesDefault,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&variables.CmdVariablesList,
|
||||||
|
&variables.CmdVariablesSet,
|
||||||
|
&variables.CmdVariablesDelete,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVariablesDefault(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
return variables.RunVariablesList(ctx, cmd)
|
||||||
|
}
|
||||||
66
cmd/actions/variables/delete.go
Normal file
66
cmd/actions/variables/delete.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package variables
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdVariablesDelete represents a sub command to delete action variables
|
||||||
|
var CmdVariablesDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"remove", "rm"},
|
||||||
|
Usage: "Delete an action variable",
|
||||||
|
Description: "Delete a variable used by repository actions",
|
||||||
|
ArgsUsage: "<variable-name>",
|
||||||
|
Action: runVariablesDelete,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Aliases: []string{"y"},
|
||||||
|
Usage: "confirm deletion without prompting",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVariablesDelete(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("variable name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
variableName := cmd.Args().First()
|
||||||
|
|
||||||
|
if !cmd.Bool("confirm") {
|
||||||
|
fmt.Printf("Are you sure you want to delete variable '%s'? [y/N] ", variableName)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "Y" && response != "yes" {
|
||||||
|
fmt.Println("Deletion canceled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DeleteRepoActionVariable(c.Owner, c.Repo, variableName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Variable '%s' deleted successfully\n", variableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
98
cmd/actions/variables/delete_test.go
Normal file
98
cmd/actions/variables/delete_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package variables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVariablesDeleteValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid variable name",
|
||||||
|
args: []string{"VALID_VARIABLE"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid lowercase name",
|
||||||
|
args: []string{"valid_variable"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no args",
|
||||||
|
args: []string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many args",
|
||||||
|
args: []string{"VARIABLE1", "VARIABLE2"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid variable name",
|
||||||
|
args: []string{"invalid-variable"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateVariableDeleteArgs(tt.args)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateVariableDeleteArgs() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVariablesDeleteFlags(t *testing.T) {
|
||||||
|
cmd := CmdVariablesDelete
|
||||||
|
|
||||||
|
// Test command properties
|
||||||
|
if cmd.Name != "delete" {
|
||||||
|
t.Errorf("Expected command name 'delete', got %s", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that rm is one of the aliases
|
||||||
|
hasRmAlias := false
|
||||||
|
for _, alias := range cmd.Aliases {
|
||||||
|
if alias == "rm" {
|
||||||
|
hasRmAlias = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRmAlias {
|
||||||
|
t.Error("Expected 'rm' to be one of the aliases for delete command")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.ArgsUsage != "<variable-name>" {
|
||||||
|
t.Errorf("Expected ArgsUsage '<variable-name>', got %s", cmd.ArgsUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Usage == "" {
|
||||||
|
t.Error("Delete command should have usage text")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("Delete command should have description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateVariableDeleteArgs validates arguments for the delete command
|
||||||
|
func validateVariableDeleteArgs(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("variable name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
return fmt.Errorf("only one variable name allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateVariableName(args[0])
|
||||||
|
}
|
||||||
61
cmd/actions/variables/list.go
Normal file
61
cmd/actions/variables/list.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package variables
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdVariablesList represents a sub command to list action variables
|
||||||
|
var CmdVariablesList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List action variables",
|
||||||
|
Description: "List variables configured for repository actions",
|
||||||
|
Action: RunVariablesList,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Usage: "show specific variable by name",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunVariablesList list action variables
|
||||||
|
func RunVariablesList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
if name := cmd.String("name"); name != "" {
|
||||||
|
// Get specific variable
|
||||||
|
variable, _, err := client.GetRepoActionVariable(c.Owner, c.Repo, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
print.ActionVariableDetails(variable)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all variables - Note: SDK doesn't have ListRepoActionVariables yet
|
||||||
|
// This is a limitation of the current SDK
|
||||||
|
fmt.Println("Note: Listing all variables is not yet supported by the Gitea SDK.")
|
||||||
|
fmt.Println("Use 'tea actions variables list --name <variable-name>' to get a specific variable.")
|
||||||
|
fmt.Println("You can also check your repository's Actions settings in the web interface.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
98
cmd/actions/variables/list_test.go
Normal file
98
cmd/actions/variables/list_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package variables
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVariablesListFlags(t *testing.T) {
|
||||||
|
cmd := CmdVariablesList
|
||||||
|
|
||||||
|
// Test that required flags exist
|
||||||
|
expectedFlags := []string{"output", "remote", "login", "repo"}
|
||||||
|
|
||||||
|
for _, flagName := range expectedFlags {
|
||||||
|
found := false
|
||||||
|
for _, flag := range cmd.Flags {
|
||||||
|
if flag.Names()[0] == flagName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected flag %s not found in CmdVariablesList", flagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test command properties
|
||||||
|
if cmd.Name != "list" {
|
||||||
|
t.Errorf("Expected command name 'list', got %s", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "ls" {
|
||||||
|
t.Errorf("Expected alias 'ls' for list command")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Usage == "" {
|
||||||
|
t.Error("List command should have usage text")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("List command should have description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVariablesListValidation(t *testing.T) {
|
||||||
|
// Basic validation that the command accepts the expected arguments
|
||||||
|
// More detailed testing would require mocking the Gitea client
|
||||||
|
|
||||||
|
// Test that list command doesn't require arguments
|
||||||
|
args := []string{}
|
||||||
|
if len(args) > 0 {
|
||||||
|
t.Error("List command should not require arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that extra arguments are ignored
|
||||||
|
extraArgs := []string{"extra", "args"}
|
||||||
|
if len(extraArgs) > 0 {
|
||||||
|
// This is fine - list commands typically ignore extra args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunVariablesListRequiresRepoContext(t *testing.T) {
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, os.Chdir(t.TempDir()))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, os.Chdir(oldWd))
|
||||||
|
})
|
||||||
|
|
||||||
|
config.SetConfigForTesting(config.LocalConfig{
|
||||||
|
Logins: []config.Login{{
|
||||||
|
Name: "test",
|
||||||
|
URL: "https://gitea.example.com",
|
||||||
|
Token: "token",
|
||||||
|
User: "tester",
|
||||||
|
Default: true,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Name: CmdVariablesList.Name,
|
||||||
|
Flags: CmdVariablesList.Flags,
|
||||||
|
}
|
||||||
|
require.NoError(t, cmd.Set("login", "test"))
|
||||||
|
|
||||||
|
err = RunVariablesList(stdctx.Background(), cmd)
|
||||||
|
require.ErrorContains(t, err, "remote repository required")
|
||||||
|
}
|
||||||
108
cmd/actions/variables/set.go
Normal file
108
cmd/actions/variables/set.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package variables
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdVariablesSet represents a sub command to set action variables
|
||||||
|
var CmdVariablesSet = cli.Command{
|
||||||
|
Name: "set",
|
||||||
|
Aliases: []string{"create", "update"},
|
||||||
|
Usage: "Set an action variable",
|
||||||
|
Description: "Set a variable for use in repository actions and workflows",
|
||||||
|
ArgsUsage: "<variable-name> [variable-value]",
|
||||||
|
Action: runVariablesSet,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "file",
|
||||||
|
Usage: "read variable value from file",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "stdin",
|
||||||
|
Usage: "read variable value from stdin",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVariablesSet(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("variable name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
variableName := cmd.Args().First()
|
||||||
|
if err := validateVariableName(variableName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read variable value using the utility
|
||||||
|
variableValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
|
||||||
|
ResourceName: "variable",
|
||||||
|
PromptMsg: fmt.Sprintf("Enter variable value for '%s'", variableName),
|
||||||
|
Hidden: false,
|
||||||
|
AllowEmpty: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateVariableValue(variableValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.CreateRepoActionVariable(c.Owner, c.Repo, variableName, variableValue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Variable '%s' set successfully\n", variableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateVariableName validates that a variable name follows the required format
|
||||||
|
func validateVariableName(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("variable name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable names can contain letters (upper/lower), numbers, and underscores
|
||||||
|
// Cannot start with a number
|
||||||
|
// Cannot contain spaces or special characters (except underscore)
|
||||||
|
validPattern := regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||||
|
if !validPattern.MatchString(name) {
|
||||||
|
return fmt.Errorf("variable name must contain only letters, numbers, and underscores, and cannot start with a number")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateVariableValue validates that a variable value is acceptable
|
||||||
|
func validateVariableValue(value string) error {
|
||||||
|
// Variables can be empty or contain whitespace, unlike secrets
|
||||||
|
|
||||||
|
// Check for maximum size (64KB limit)
|
||||||
|
if len(value) > 65536 {
|
||||||
|
return fmt.Errorf("variable value cannot exceed 64KB")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
213
cmd/actions/variables/set_test.go
Normal file
213
cmd/actions/variables/set_test.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package variables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateVariableName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid name",
|
||||||
|
input: "VALID_VARIABLE_NAME",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid name with numbers",
|
||||||
|
input: "VARIABLE_123",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid lowercase",
|
||||||
|
input: "valid_variable",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid mixed case",
|
||||||
|
input: "Mixed_Case_Variable",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - spaces",
|
||||||
|
input: "INVALID VARIABLE",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - special chars",
|
||||||
|
input: "INVALID-VARIABLE!",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - starts with number",
|
||||||
|
input: "1INVALID",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - empty",
|
||||||
|
input: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateVariableName(tt.input)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateVariableName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVariableSourceArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid args",
|
||||||
|
args: []string{"VALID_VARIABLE", "variable_value"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid lowercase",
|
||||||
|
args: []string{"valid_variable", "value"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing name",
|
||||||
|
args: []string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many args",
|
||||||
|
args: []string{"VARIABLE_NAME", "value", "extra"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid variable name",
|
||||||
|
args: []string{"invalid-variable", "value"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Test argument validation only
|
||||||
|
if len(tt.args) == 0 {
|
||||||
|
if !tt.wantErr {
|
||||||
|
t.Error("Expected error for empty args")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tt.args) > 2 {
|
||||||
|
if !tt.wantErr {
|
||||||
|
t.Error("Expected error for too many args")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test variable name validation
|
||||||
|
err := validateVariableName(tt.args[0])
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateVariableName() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVariableNameValidation(t *testing.T) {
|
||||||
|
// Test that variable names follow GitHub Actions/Gitea Actions conventions
|
||||||
|
validNames := []string{
|
||||||
|
"VALID_VARIABLE",
|
||||||
|
"API_URL",
|
||||||
|
"DATABASE_HOST",
|
||||||
|
"VARIABLE_123",
|
||||||
|
"mixed_Case_Variable",
|
||||||
|
"lowercase_variable",
|
||||||
|
"UPPERCASE_VARIABLE",
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidNames := []string{
|
||||||
|
"Invalid-Dashes",
|
||||||
|
"INVALID SPACES",
|
||||||
|
"123_STARTS_WITH_NUMBER",
|
||||||
|
"", // Empty
|
||||||
|
"INVALID!@#", // Special chars
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range validNames {
|
||||||
|
t.Run("valid_"+name, func(t *testing.T) {
|
||||||
|
err := validateVariableName(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("validateVariableName(%q) should be valid, got error: %v", name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range invalidNames {
|
||||||
|
t.Run("invalid_"+name, func(t *testing.T) {
|
||||||
|
err := validateVariableName(name)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("validateVariableName(%q) should be invalid, got no error", name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVariableValueValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid value",
|
||||||
|
value: "variable123",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid complex value",
|
||||||
|
value: "https://api.example.com/v1",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid multiline value",
|
||||||
|
value: "line1\nline2\nline3",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty value allowed",
|
||||||
|
value: "",
|
||||||
|
wantErr: false, // Variables can be empty unlike secrets
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace only allowed",
|
||||||
|
value: " \t\n ",
|
||||||
|
wantErr: false, // Variables can contain whitespace
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "very long value",
|
||||||
|
value: strings.Repeat("a", 65537), // Over 64KB
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateVariableValue(tt.value)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateVariableValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
32
cmd/actions/workflows.go
Normal file
32
cmd/actions/workflows.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/actions/workflows"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdActionsWorkflows represents the actions workflows command
|
||||||
|
var CmdActionsWorkflows = cli.Command{
|
||||||
|
Name: "workflows",
|
||||||
|
Aliases: []string{"workflow"},
|
||||||
|
Usage: "Manage repository workflows",
|
||||||
|
Description: "List and manage repository action workflows",
|
||||||
|
Action: runWorkflowsDefault,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&workflows.CmdWorkflowsList,
|
||||||
|
&workflows.CmdWorkflowsView,
|
||||||
|
&workflows.CmdWorkflowsDispatch,
|
||||||
|
&workflows.CmdWorkflowsEnable,
|
||||||
|
&workflows.CmdWorkflowsDisable,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsDefault(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
return workflows.RunWorkflowsList(ctx, cmd)
|
||||||
|
}
|
||||||
65
cmd/actions/workflows/disable.go
Normal file
65
cmd/actions/workflows/disable.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsDisable represents a sub command to disable a workflow
|
||||||
|
var CmdWorkflowsDisable = cli.Command{
|
||||||
|
Name: "disable",
|
||||||
|
Usage: "Disable a workflow",
|
||||||
|
Description: "Disable a workflow in the repository",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsDisable,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Aliases: []string{"y"},
|
||||||
|
Usage: "confirm disable without prompting",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsDisable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
|
||||||
|
if !cmd.Bool("confirm") {
|
||||||
|
fmt.Printf("Are you sure you want to disable workflow %s? [y/N] ", workflowID)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "Y" && response != "yes" {
|
||||||
|
fmt.Println("Disable canceled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DisableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to disable workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Workflow %s disabled successfully\n", workflowID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
174
cmd/actions/workflows/dispatch.go
Normal file
174
cmd/actions/workflows/dispatch.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsDispatch represents a sub command to dispatch a workflow
|
||||||
|
var CmdWorkflowsDispatch = cli.Command{
|
||||||
|
Name: "dispatch",
|
||||||
|
Aliases: []string{"trigger", "run"},
|
||||||
|
Usage: "Dispatch a workflow run",
|
||||||
|
Description: "Trigger a workflow_dispatch event for a workflow",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsDispatch,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "ref",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "branch or tag to dispatch on (default: current branch)",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "input",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "workflow input in key=value format (can be specified multiple times)",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "follow",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "follow log output after dispatching",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsDispatch(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
|
||||||
|
ref := cmd.String("ref")
|
||||||
|
if ref == "" {
|
||||||
|
if c.LocalRepo != nil {
|
||||||
|
branchName, _, localErr := c.LocalRepo.TeaGetCurrentBranchNameAndSHA()
|
||||||
|
if localErr == nil && branchName != "" {
|
||||||
|
ref = branchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref == "" {
|
||||||
|
return fmt.Errorf("--ref is required (no local branch detected)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := make(map[string]string)
|
||||||
|
for _, input := range cmd.StringSlice("input") {
|
||||||
|
key, value, ok := strings.Cut(input, "=")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid input format %q, expected key=value", input)
|
||||||
|
}
|
||||||
|
inputs[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea.CreateActionWorkflowDispatchOption{
|
||||||
|
Ref: ref,
|
||||||
|
Inputs: inputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
details, _, err := client.DispatchRepoActionWorkflow(c.Owner, c.Repo, workflowID, opt, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to dispatch workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print.ActionWorkflowDispatchResult(details)
|
||||||
|
|
||||||
|
if cmd.Bool("follow") && details != nil && details.WorkflowRunID > 0 {
|
||||||
|
return followDispatchedRun(client, c, details.WorkflowRunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
followPollInterval = 2 * time.Second
|
||||||
|
followMaxDuration = 30 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// followDispatchedRun waits for the dispatched run to start, then follows its logs
|
||||||
|
func followDispatchedRun(client *gitea.Client, c *context.TeaContext, runID int64) error {
|
||||||
|
fmt.Printf("\nWaiting for run %d to start...\n", runID)
|
||||||
|
|
||||||
|
var jobs *gitea.ActionWorkflowJobsResponse
|
||||||
|
for range 30 {
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
jobs, _, err = client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get jobs: %w", err)
|
||||||
|
}
|
||||||
|
if len(jobs.Jobs) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobs == nil || len(jobs.Jobs) == 0 {
|
||||||
|
return fmt.Errorf("timed out waiting for jobs to appear")
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := jobs.Jobs[0].ID
|
||||||
|
jobName := jobs.Jobs[0].Name
|
||||||
|
fmt.Printf("Following logs for job '%s' (ID: %d) - press Ctrl+C to stop...\n", jobName, jobID)
|
||||||
|
fmt.Println("---")
|
||||||
|
|
||||||
|
deadline := time.Now().Add(followMaxDuration)
|
||||||
|
var lastLogLength int
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
job, _, err := client.GetRepoActionJob(c.Owner, c.Repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning := job.Status == "in_progress" || job.Status == "queued" || job.Status == "pending"
|
||||||
|
|
||||||
|
logs, _, logErr := client.GetRepoActionJobLogs(c.Owner, c.Repo, jobID)
|
||||||
|
if logErr != nil && isRunning {
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if logErr == nil && len(logs) > lastLogLength {
|
||||||
|
fmt.Print(string(logs[lastLogLength:]))
|
||||||
|
lastLogLength = len(logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isRunning {
|
||||||
|
if logErr != nil {
|
||||||
|
fmt.Printf("\n---\nJob completed with status: %s (failed to fetch final logs: %v)\n", job.Status, logErr)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n---\nJob completed with status: %s\n", job.Status)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return fmt.Errorf("timed out after %s following logs", followMaxDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
cmd/actions/workflows/enable.go
Normal file
48
cmd/actions/workflows/enable.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsEnable represents a sub command to enable a workflow
|
||||||
|
var CmdWorkflowsEnable = cli.Command{
|
||||||
|
Name: "enable",
|
||||||
|
Usage: "Enable a workflow",
|
||||||
|
Description: "Enable a disabled workflow in the repository",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsEnable,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsEnable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
_, err = client.EnableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to enable workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Workflow %s enabled successfully\n", workflowID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
50
cmd/actions/workflows/list.go
Normal file
50
cmd/actions/workflows/list.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsList represents a sub command to list workflows
|
||||||
|
var CmdWorkflowsList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List repository workflows",
|
||||||
|
Description: "List workflows in the repository with their status",
|
||||||
|
Action: RunWorkflowsList,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunWorkflowsList lists workflows in the repository using the workflow API
|
||||||
|
func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
resp, _, err := client.ListRepoActionWorkflows(c.Owner, c.Repo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list workflows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var workflows []*gitea.ActionWorkflow
|
||||||
|
if resp != nil {
|
||||||
|
workflows = resp.Workflows
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.ActionWorkflowsList(workflows, c.Output)
|
||||||
|
}
|
||||||
50
cmd/actions/workflows/view.go
Normal file
50
cmd/actions/workflows/view.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsView represents a sub command to view workflow details
|
||||||
|
var CmdWorkflowsView = cli.Command{
|
||||||
|
Name: "view",
|
||||||
|
Aliases: []string{"show", "get"},
|
||||||
|
Usage: "View workflow details",
|
||||||
|
Description: "View details of a specific workflow",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsView,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsView(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
wf, _, err := client.GetRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print.ActionWorkflowDetails(wf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
59
cmd/admin.go
Normal file
59
cmd/admin.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/admin/users"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdAdmin represents the namespace of admin commands.
|
||||||
|
// The command itself has no functionality, but hosts subcommands.
|
||||||
|
var CmdAdmin = cli.Command{
|
||||||
|
Name: "admin",
|
||||||
|
Usage: "Operations requiring admin access on the Gitea instance",
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Category: catMisc,
|
||||||
|
Action: func(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
return cli.ShowSubcommandHelp(cmd)
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&cmdAdminUsers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdAdminUsers = cli.Command{
|
||||||
|
Name: "users",
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
Usage: "Manage registered users",
|
||||||
|
Action: func(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 1 {
|
||||||
|
return runAdminUserDetail(ctx, cmd, cmd.Args().First())
|
||||||
|
}
|
||||||
|
return users.RunUserList(ctx, cmd)
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&users.CmdUserList,
|
||||||
|
},
|
||||||
|
Flags: users.CmdUserList.Flags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAdminUserDetail(_ stdctx.Context, cmd *cli.Command, u string) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
user, _, err := client.GetUserInfo(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
print.UserDetails(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
cmd/admin/users/list.go
Normal file
56
cmd/admin/users/list.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var userFieldsFlag = flags.FieldsFlag(print.UserFields, []string{
|
||||||
|
"id", "login", "full_name", "email", "activated",
|
||||||
|
})
|
||||||
|
|
||||||
|
// CmdUserList represents a sub command of users to list users
|
||||||
|
var CmdUserList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List Users",
|
||||||
|
Description: "List users",
|
||||||
|
Action: RunUserList,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
userFieldsFlag,
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunUserList list users
|
||||||
|
func RunUserList(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fields, err := userFieldsFlag.GetValues(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
users, _, err := client.AdminListUsers(gitea.AdminListUsersOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.UserList(users, ctx.Output, fields)
|
||||||
|
}
|
||||||
408
cmd/api.go
Normal file
408
cmd/api.go
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
stdctx "context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/api"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// apiFlags returns a fresh set of flag instances for the api command.
|
||||||
|
// This is a factory function so that each invocation gets independent flag
|
||||||
|
// objects, avoiding shared hasBeenSet state across tests.
|
||||||
|
func apiFlags() []cli.Flag {
|
||||||
|
return []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "method",
|
||||||
|
Aliases: []string{"X"},
|
||||||
|
Usage: "HTTP method (GET, POST, PUT, PATCH, DELETE)",
|
||||||
|
Value: "GET",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "field",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "Add a string field to the request body (key=value)",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "Field",
|
||||||
|
Aliases: []string{"F"},
|
||||||
|
Usage: "Add a typed field to the request body (key=value, @file, or @- for stdin)",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "header",
|
||||||
|
Aliases: []string{"H"},
|
||||||
|
Usage: "Add a custom header (key:value)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "data",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
Usage: "Raw JSON request body (use @file to read from file, @- for stdin)",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "include",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "Include HTTP status and response headers in output (written to stderr)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "Write response body to file instead of stdout (use '-' for stdout)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmdApi represents the api command
|
||||||
|
var CmdApi = cli.Command{
|
||||||
|
Name: "api",
|
||||||
|
Category: catHelpers,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Usage: "Make an authenticated API request",
|
||||||
|
Description: `Makes an authenticated HTTP request to the Gitea API and prints the response.
|
||||||
|
|
||||||
|
The endpoint argument is the path to the API endpoint, which will be prefixed
|
||||||
|
with /api/v1/ if it doesn't start with /api/ or http(s)://.
|
||||||
|
|
||||||
|
Placeholders like {owner} and {repo} in the endpoint will be replaced with
|
||||||
|
values from the current repository context.
|
||||||
|
|
||||||
|
Use -f for string fields and -F for typed fields (numbers, booleans, null).
|
||||||
|
With -F, prefix value with @ to read from file (@- for stdin). Values starting
|
||||||
|
with [ or { are parsed as JSON arrays/objects. Wrap values in quotes to force
|
||||||
|
string type (e.g., -F key="null" for literal string "null").
|
||||||
|
|
||||||
|
Use -d/--data to send a raw JSON body. Use @file to read from a file, or @-
|
||||||
|
to read from stdin. The -d flag cannot be combined with -f or -F.
|
||||||
|
|
||||||
|
When a request body is provided via -f, -F, or -d, the method defaults to POST
|
||||||
|
unless explicitly set with -X/--method.
|
||||||
|
|
||||||
|
Note: if your endpoint contains ? or &, quote it to prevent shell expansion
|
||||||
|
(e.g., '/repos/{owner}/{repo}/issues?state=open').`,
|
||||||
|
ArgsUsage: "<endpoint>",
|
||||||
|
Action: runApi,
|
||||||
|
Flags: append(apiFlags(), flags.LoginRepoFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
type preparedAPIRequest struct {
|
||||||
|
Method string
|
||||||
|
Endpoint string
|
||||||
|
Headers map[string]string
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func runApi(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request, err := prepareAPIRequest(cmd, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var body io.Reader
|
||||||
|
if request.Body != nil {
|
||||||
|
body = bytes.NewReader(request.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create API client and make request
|
||||||
|
client := api.NewClient(ctx.Login)
|
||||||
|
resp, err := client.Do(request.Method, request.Endpoint, body, request.Headers)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := resp.Body.Close(); closeErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Print headers to stderr if requested (so redirects/pipes work correctly)
|
||||||
|
if cmd.Bool("include") {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s %s\n", resp.Proto, resp.Status)
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s\n", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine output destination
|
||||||
|
outputPath := cmd.String("output")
|
||||||
|
forceStdout := outputPath == "-"
|
||||||
|
outputToStdout := outputPath == "" || forceStdout
|
||||||
|
|
||||||
|
// Check for binary output to terminal (skip warning if user explicitly forced stdout)
|
||||||
|
if outputToStdout && !forceStdout && term.IsTerminal(int(os.Stdout.Fd())) && !isTextContentType(resp.Header.Get("Content-Type")) {
|
||||||
|
fmt.Fprintln(os.Stderr, "Warning: Binary output detected. Use '-o <file>' to save to a file,")
|
||||||
|
fmt.Fprintln(os.Stderr, "or '-o -' to force output to terminal.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var output io.Writer = os.Stdout
|
||||||
|
if !outputToStdout {
|
||||||
|
file, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create output file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := file.Close(); closeErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: failed to close output file: %v\n", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
output = file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy response body to output
|
||||||
|
_, err = io.Copy(output, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add newline for better terminal display
|
||||||
|
if outputToStdout && term.IsTerminal(int(os.Stdout.Fd())) {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareAPIRequest(cmd *cli.Command, ctx *context.TeaContext) (*preparedAPIRequest, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Get the endpoint argument
|
||||||
|
if cmd.NArg() < 1 {
|
||||||
|
return nil, fmt.Errorf("endpoint argument required")
|
||||||
|
}
|
||||||
|
endpoint := cmd.Args().First()
|
||||||
|
|
||||||
|
// Expand placeholders in endpoint
|
||||||
|
endpoint = expandPlaceholders(endpoint, ctx)
|
||||||
|
|
||||||
|
// Parse headers
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for _, h := range cmd.StringSlice("header") {
|
||||||
|
parts := strings.SplitN(h, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid header format: %q (expected key:value)", h)
|
||||||
|
}
|
||||||
|
headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request body from fields
|
||||||
|
var bodyBytes []byte
|
||||||
|
stringFields := cmd.StringSlice("field")
|
||||||
|
typedFields := cmd.StringSlice("Field")
|
||||||
|
dataRaw := cmd.String("data")
|
||||||
|
|
||||||
|
if dataRaw != "" && (len(stringFields) > 0 || len(typedFields) > 0) {
|
||||||
|
return nil, fmt.Errorf("--data/-d cannot be combined with --field/-f or --Field/-F")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataRaw != "" {
|
||||||
|
var dataBytes []byte
|
||||||
|
var dataSource string
|
||||||
|
if strings.HasPrefix(dataRaw, "@") {
|
||||||
|
filename := dataRaw[1:]
|
||||||
|
if filename == "-" {
|
||||||
|
dataBytes, err = io.ReadAll(os.Stdin)
|
||||||
|
dataSource = "stdin"
|
||||||
|
} else {
|
||||||
|
dataBytes, err = os.ReadFile(filename)
|
||||||
|
dataSource = filename
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read %q: %w", dataRaw, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataBytes = []byte(dataRaw)
|
||||||
|
}
|
||||||
|
if !json.Valid(dataBytes) {
|
||||||
|
if dataSource != "" {
|
||||||
|
return nil, fmt.Errorf("--data/-d value from %s is not valid JSON", dataSource)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("--data/-d value is not valid JSON")
|
||||||
|
}
|
||||||
|
bodyBytes = dataBytes
|
||||||
|
} else if len(stringFields) > 0 || len(typedFields) > 0 {
|
||||||
|
bodyMap := make(map[string]any)
|
||||||
|
|
||||||
|
// Process string fields (-f)
|
||||||
|
for _, f := range stringFields {
|
||||||
|
parts := strings.SplitN(f, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid field format: %q (expected key=value)", f)
|
||||||
|
}
|
||||||
|
key := parts[0]
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("field key cannot be empty in %q", f)
|
||||||
|
}
|
||||||
|
if _, exists := bodyMap[key]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate field key %q", key)
|
||||||
|
}
|
||||||
|
bodyMap[key] = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process typed fields (-F)
|
||||||
|
for _, f := range typedFields {
|
||||||
|
parts := strings.SplitN(f, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid field format: %q (expected key=value)", f)
|
||||||
|
}
|
||||||
|
key := parts[0]
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("field key cannot be empty in %q", f)
|
||||||
|
}
|
||||||
|
if _, exists := bodyMap[key]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate field key %q", key)
|
||||||
|
}
|
||||||
|
value := parts[1]
|
||||||
|
|
||||||
|
parsedValue, err := parseTypedValue(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse field %q: %w", key, err)
|
||||||
|
}
|
||||||
|
bodyMap[key] = parsedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err = json.Marshal(bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode request body: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
method := strings.ToUpper(cmd.String("method"))
|
||||||
|
if !cmd.IsSet("method") {
|
||||||
|
if bodyBytes != nil {
|
||||||
|
method = "POST"
|
||||||
|
} else {
|
||||||
|
method = "GET"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &preparedAPIRequest{
|
||||||
|
Method: method,
|
||||||
|
Endpoint: endpoint,
|
||||||
|
Headers: headers,
|
||||||
|
Body: bodyBytes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTypedValue parses a value for -F flag, handling:
|
||||||
|
// - @filename: read content from file
|
||||||
|
// - @-: read content from stdin
|
||||||
|
// - "quoted": literal string (prevents type parsing)
|
||||||
|
// - true/false: boolean
|
||||||
|
// - null: nil
|
||||||
|
// - numbers: int or float
|
||||||
|
// - []/{}: JSON arrays/objects
|
||||||
|
// - otherwise: string
|
||||||
|
func parseTypedValue(value string) (any, error) {
|
||||||
|
// Handle file references.
|
||||||
|
// Note: if multiple fields use @- (stdin), only the first will get data;
|
||||||
|
// subsequent reads will return empty since stdin is consumed once.
|
||||||
|
if strings.HasPrefix(value, "@") {
|
||||||
|
filename := value[1:]
|
||||||
|
var content []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if filename == "-" {
|
||||||
|
content, err = io.ReadAll(os.Stdin)
|
||||||
|
} else {
|
||||||
|
content, err = os.ReadFile(filename)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read %q: %w", value, err)
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(string(content), "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle quoted strings (literal strings, no type parsing).
|
||||||
|
// Uses strconv.Unquote so escape sequences like \" are handled correctly.
|
||||||
|
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
|
||||||
|
unquoted, err := strconv.Unquote(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid quoted string %s: %w", value, err)
|
||||||
|
}
|
||||||
|
return unquoted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle null
|
||||||
|
if value == "null" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle booleans
|
||||||
|
if value == "true" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if value == "false" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle integers
|
||||||
|
if i, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle floats
|
||||||
|
if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSON arrays and objects
|
||||||
|
if len(value) > 0 && (value[0] == '[' || value[0] == '{') {
|
||||||
|
var jsonVal any
|
||||||
|
if err := json.Unmarshal([]byte(value), &jsonVal); err == nil {
|
||||||
|
return jsonVal, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to string
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTextContentType returns true if the content type indicates text data
|
||||||
|
func isTextContentType(contentType string) bool {
|
||||||
|
if contentType == "" {
|
||||||
|
return true // assume text if unknown
|
||||||
|
}
|
||||||
|
contentType = strings.ToLower(strings.Split(contentType, ";")[0]) // strip charset
|
||||||
|
|
||||||
|
return strings.HasPrefix(contentType, "text/") ||
|
||||||
|
strings.Contains(contentType, "json") ||
|
||||||
|
strings.Contains(contentType, "xml") ||
|
||||||
|
strings.Contains(contentType, "javascript") ||
|
||||||
|
strings.Contains(contentType, "yaml") ||
|
||||||
|
strings.Contains(contentType, "toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandPlaceholders replaces {owner}, {repo}, and {branch} in the endpoint
|
||||||
|
func expandPlaceholders(endpoint string, ctx *context.TeaContext) string {
|
||||||
|
endpoint = strings.ReplaceAll(endpoint, "{owner}", ctx.Owner)
|
||||||
|
endpoint = strings.ReplaceAll(endpoint, "{repo}", ctx.Repo)
|
||||||
|
|
||||||
|
// Get current branch if available
|
||||||
|
if ctx.LocalRepo != nil {
|
||||||
|
if branch, err := ctx.LocalRepo.Head(); err == nil {
|
||||||
|
branchName := branch.Name().Short()
|
||||||
|
endpoint = strings.ReplaceAll(endpoint, "{branch}", branchName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
635
cmd/api_test.go
Normal file
635
cmd/api_test.go
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
tea_git "code.gitea.io/tea/modules/git"
|
||||||
|
|
||||||
|
gogit "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseTypedValue(t *testing.T) {
|
||||||
|
t.Run("null", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("null")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bool true", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("true")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, true, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bool false", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("false")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, false, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("integer", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("42")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(42), v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("float", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("3.14")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 3.14, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("hello")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON array", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("[1,2,3]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []any{float64(1), float64(2), float64(3)}, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON object", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`{"key":"val"}`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{"key": "val"}, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON array falls back to string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("[not json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "[not json", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON object falls back to string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("{not json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "{not json", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("file reference", func(t *testing.T) {
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "test.txt")
|
||||||
|
require.NoError(t, os.WriteFile(tmpFile, []byte("file content\n"), 0o644))
|
||||||
|
v, err := parseTypedValue("@" + tmpFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "file content", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("file reference without trailing newline", func(t *testing.T) {
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "test.txt")
|
||||||
|
require.NoError(t, os.WriteFile(tmpFile, []byte("no newline"), 0o644))
|
||||||
|
v, err := parseTypedValue("@" + tmpFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "no newline", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty file reference", func(t *testing.T) {
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "empty.txt")
|
||||||
|
require.NoError(t, os.WriteFile(tmpFile, []byte(""), 0o644))
|
||||||
|
v, err := parseTypedValue("@" + tmpFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonexistent file reference", func(t *testing.T) {
|
||||||
|
_, err := parseTypedValue("@/nonexistent/file.txt")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to read")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative integer", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("-42")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(-42), v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative float", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("-3.14")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, -3.14, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scientific notation", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("1.5e10")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1.5e10, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string starting with number", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue("123abc")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "123abc", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nested JSON object", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`{"user":{"name":"alice","id":1}}`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := map[string]any{
|
||||||
|
"user": map[string]any{
|
||||||
|
"name": "alice",
|
||||||
|
"id": float64(1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("complex JSON array", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`[{"id":1},{"id":2}]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := []any{
|
||||||
|
map[string]any{"id": float64(1)},
|
||||||
|
map[string]any{"id": float64(2)},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted string prevents type parsing", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"null"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "null", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted true becomes string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"true"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "true", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted false becomes string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"false"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "false", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted number becomes string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"123"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "123", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted empty string", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`""`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted string with spaces", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"hello world"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello world", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single quote not treated as quote", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`'hello'`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "'hello'", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unmatched quote at start only", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"hello`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, `"hello`, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unmatched quote at end only", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`hello"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, `hello"`, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted string with escaped quote", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"hello \"world\""`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, `hello "world"`, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted string with backslash-n", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"line1\nline2"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "line1\nline2", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted string with tab escape", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"col1\tcol2"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "col1\tcol2", v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quoted string with backslash", func(t *testing.T) {
|
||||||
|
v, err := parseTypedValue(`"path\\to\\file"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, `path\to\file`, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid escape sequence in quoted string", func(t *testing.T) {
|
||||||
|
_, err := parseTypedValue(`"bad \z escape"`)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "invalid quoted string")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// runApiWithArgs configures a test login, parses the command line, and captures
|
||||||
|
// the prepared request without opening sockets or making HTTP requests.
|
||||||
|
func runApiWithArgs(t *testing.T, args []string) (method string, body []byte, err error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var capturedMethod string
|
||||||
|
var capturedBody []byte
|
||||||
|
|
||||||
|
config.SetConfigForTesting(config.LocalConfig{
|
||||||
|
Logins: []config.Login{{
|
||||||
|
Name: "testLogin",
|
||||||
|
URL: "https://gitea.example.com",
|
||||||
|
Token: "test-token",
|
||||||
|
User: "testUser",
|
||||||
|
Default: true,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use the apiFlags factory to get fresh flag instances, avoiding shared
|
||||||
|
// hasBeenSet state between tests. Append minimal login/repo flags needed
|
||||||
|
// for the test harness.
|
||||||
|
cmd := cli.Command{
|
||||||
|
Name: "api",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request, err := prepareAPIRequest(cmd, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
capturedMethod = request.Method
|
||||||
|
capturedBody = append([]byte(nil), request.Body...)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Flags: append(apiFlags(), []cli.Flag{
|
||||||
|
&cli.StringFlag{Name: "login", Aliases: []string{"l"}},
|
||||||
|
&cli.StringFlag{Name: "repo", Aliases: []string{"r"}},
|
||||||
|
&cli.StringFlag{Name: "remote", Aliases: []string{"R"}},
|
||||||
|
}...),
|
||||||
|
Writer: io.Discard,
|
||||||
|
ErrWriter: io.Discard,
|
||||||
|
}
|
||||||
|
|
||||||
|
fullArgs := append([]string{"api", "--login", "testLogin"}, args...)
|
||||||
|
runErr := cmd.Run(stdctx.Background(), fullArgs)
|
||||||
|
|
||||||
|
return capturedMethod, capturedBody, runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiCommaInFieldValue(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{"-f", "body=hello, world", "-X", "POST", "/test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, "hello, world", parsed["body"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiRawDataFlag(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{"-d", `{"title":"test","body":"hello"}`, "/test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, "test", parsed["title"])
|
||||||
|
assert.Equal(t, "hello", parsed["body"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiDataFieldMutualExclusion(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-f", "key=val", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "--data/-d cannot be combined with --field/-f or --Field/-F")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiMethodAutoDefault(t *testing.T) {
|
||||||
|
t.Run("POST when body provided without explicit method", func(t *testing.T) {
|
||||||
|
method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "/test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "POST", method)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit method overrides auto-POST", func(t *testing.T) {
|
||||||
|
method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-X", "PATCH", "/test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "PATCH", method)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GET when no body", func(t *testing.T) {
|
||||||
|
method, _, err := runApiWithArgs(t, []string{"/test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "GET", method)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiMultipleFields(t *testing.T) {
|
||||||
|
t.Run("multiple -f flags", func(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{
|
||||||
|
"-f", "title=Test Issue",
|
||||||
|
"-f", "body=Description here",
|
||||||
|
"-X", "POST",
|
||||||
|
"/test",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, "Test Issue", parsed["title"])
|
||||||
|
assert.Equal(t, "Description here", parsed["body"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple -F flags with different types", func(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{
|
||||||
|
"-F", "milestone=5",
|
||||||
|
"-F", "closed=true",
|
||||||
|
"-F", "title=Test",
|
||||||
|
"-X", "POST",
|
||||||
|
"/test",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, float64(5), parsed["milestone"])
|
||||||
|
assert.Equal(t, true, parsed["closed"])
|
||||||
|
assert.Equal(t, "Test", parsed["title"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("combining -f and -F flags", func(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{
|
||||||
|
"-f", "title=Test",
|
||||||
|
"-F", "milestone=3",
|
||||||
|
"-F", "closed=false",
|
||||||
|
"-X", "POST",
|
||||||
|
"/test",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, "Test", parsed["title"])
|
||||||
|
assert.Equal(t, float64(3), parsed["milestone"])
|
||||||
|
assert.Equal(t, false, parsed["closed"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("-F with JSON array", func(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{
|
||||||
|
"-F", `labels=["bug","enhancement"]`,
|
||||||
|
"-X", "POST",
|
||||||
|
"/test",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, []any{"bug", "enhancement"}, parsed["labels"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("-F with JSON object", func(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{
|
||||||
|
"-F", `assignee={"login":"alice","id":123}`,
|
||||||
|
"-X", "POST",
|
||||||
|
"/test",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assignee, ok := parsed["assignee"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "alice", assignee["login"])
|
||||||
|
assert.Equal(t, float64(123), assignee["id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("-F with quoted string to prevent type parsing", func(t *testing.T) {
|
||||||
|
_, body, err := runApiWithArgs(t, []string{
|
||||||
|
"-F", `status="null"`,
|
||||||
|
"-F", `enabled="true"`,
|
||||||
|
"-F", `count="42"`,
|
||||||
|
"-X", "POST",
|
||||||
|
"/test",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, "null", parsed["status"])
|
||||||
|
assert.Equal(t, "true", parsed["enabled"])
|
||||||
|
assert.Equal(t, "42", parsed["count"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiDataFromFile(t *testing.T) {
|
||||||
|
t.Run("read JSON from file", func(t *testing.T) {
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "data.json")
|
||||||
|
jsonData := `{"title":"From File","body":"File content"}`
|
||||||
|
require.NoError(t, os.WriteFile(tmpFile, []byte(jsonData), 0o644))
|
||||||
|
|
||||||
|
_, body, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(body, &parsed))
|
||||||
|
assert.Equal(t, "From File", parsed["title"])
|
||||||
|
assert.Equal(t, "File content", parsed["body"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON in --data flag", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-d", `{invalid json}`, "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not valid JSON")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON from file includes filename", func(t *testing.T) {
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "bad.json")
|
||||||
|
require.NoError(t, os.WriteFile(tmpFile, []byte("not json"), 0o644))
|
||||||
|
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not valid JSON")
|
||||||
|
assert.Contains(t, err.Error(), "bad.json")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiErrorHandling(t *testing.T) {
|
||||||
|
t.Run("missing endpoint argument", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "endpoint argument required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid field format", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-f", "invalidformat", "-X", "POST", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "invalid field format")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid Field format", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-F", "noequalsign", "-X", "POST", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "invalid field format")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty field key with -f", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-f", "=value", "-X", "POST", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "field key cannot be empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty field key with -F", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-F", "=123", "-X", "POST", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "field key cannot be empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("duplicate field key in -f flags", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-f", "key=first", "-f", "key=second", "-X", "POST", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "duplicate field key")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("duplicate field key in -F flags", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-F", "key=1", "-F", "key=2", "-X", "POST", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "duplicate field key")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("duplicate field key across -f and -F flags", func(t *testing.T) {
|
||||||
|
_, _, err := runApiWithArgs(t, []string{"-f", "key=string", "-F", "key=123", "-X", "POST", "/test"})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "duplicate field key")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandPlaceholders(t *testing.T) {
|
||||||
|
t.Run("replaces owner and repo", func(t *testing.T) {
|
||||||
|
ctx := &context.TeaContext{
|
||||||
|
Owner: "myorg",
|
||||||
|
Repo: "myrepo",
|
||||||
|
}
|
||||||
|
result := expandPlaceholders("/repos/{owner}/{repo}/issues", ctx)
|
||||||
|
assert.Equal(t, "/repos/myorg/myrepo/issues", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replaces multiple occurrences", func(t *testing.T) {
|
||||||
|
ctx := &context.TeaContext{
|
||||||
|
Owner: "alice",
|
||||||
|
Repo: "proj",
|
||||||
|
}
|
||||||
|
result := expandPlaceholders("/repos/{owner}/{repo}/branches?owner={owner}", ctx)
|
||||||
|
assert.Equal(t, "/repos/alice/proj/branches?owner=alice", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no placeholders returns unchanged", func(t *testing.T) {
|
||||||
|
ctx := &context.TeaContext{
|
||||||
|
Owner: "alice",
|
||||||
|
Repo: "proj",
|
||||||
|
}
|
||||||
|
result := expandPlaceholders("/api/v1/version", ctx)
|
||||||
|
assert.Equal(t, "/api/v1/version", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty owner and repo produce empty replacements", func(t *testing.T) {
|
||||||
|
ctx := &context.TeaContext{}
|
||||||
|
result := expandPlaceholders("/repos/{owner}/{repo}", ctx)
|
||||||
|
assert.Equal(t, "/repos//", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("branch left unreplaced when no local repo", func(t *testing.T) {
|
||||||
|
ctx := &context.TeaContext{
|
||||||
|
Owner: "alice",
|
||||||
|
Repo: "proj",
|
||||||
|
}
|
||||||
|
result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
|
||||||
|
assert.Equal(t, "/repos/alice/proj/branches/{branch}", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replaces branch from local repo HEAD", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
repo, err := gogit.PlainInit(tmpDir, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create an initial commit so HEAD points to a branch.
|
||||||
|
wt, err := repo.Worktree()
|
||||||
|
require.NoError(t, err)
|
||||||
|
tmpFile := filepath.Join(tmpDir, "init.txt")
|
||||||
|
require.NoError(t, os.WriteFile(tmpFile, []byte("init"), 0o644))
|
||||||
|
_, err = wt.Add("init.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = wt.Commit("initial commit", &gogit.CommitOptions{
|
||||||
|
Author: &object.Signature{Name: "test", Email: "test@test.com"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create and checkout a feature branch.
|
||||||
|
headRef, err := repo.Head()
|
||||||
|
require.NoError(t, err)
|
||||||
|
branchRef := plumbing.NewBranchReferenceName("feature/my-branch")
|
||||||
|
ref := plumbing.NewHashReference(branchRef, headRef.Hash())
|
||||||
|
require.NoError(t, repo.Storer.SetReference(ref))
|
||||||
|
require.NoError(t, wt.Checkout(&gogit.CheckoutOptions{Branch: branchRef}))
|
||||||
|
|
||||||
|
ctx := &context.TeaContext{
|
||||||
|
Owner: "alice",
|
||||||
|
Repo: "proj",
|
||||||
|
LocalRepo: &tea_git.TeaRepo{Repository: repo},
|
||||||
|
}
|
||||||
|
result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
|
||||||
|
assert.Equal(t, "/repos/alice/proj/branches/feature/my-branch", result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsTextContentType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
contentType string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"empty string defaults to text", "", true},
|
||||||
|
{"plain text", "text/plain", true},
|
||||||
|
{"html", "text/html", true},
|
||||||
|
{"json", "application/json", true},
|
||||||
|
{"json with charset", "application/json; charset=utf-8", true},
|
||||||
|
{"xml", "application/xml", true},
|
||||||
|
{"javascript", "application/javascript", true},
|
||||||
|
{"yaml", "application/yaml", true},
|
||||||
|
{"toml", "application/toml", true},
|
||||||
|
{"binary", "application/octet-stream", false},
|
||||||
|
{"image", "image/png", false},
|
||||||
|
{"pdf", "application/pdf", false},
|
||||||
|
{"zip", "application/zip", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := isTextContentType(tt.contentType)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
28
cmd/attachments.go
Normal file
28
cmd/attachments.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/tea/cmd/attachments"
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdReleaseAttachments represents a release attachment (file attachment)
|
||||||
|
var CmdReleaseAttachments = cli.Command{
|
||||||
|
Name: "assets",
|
||||||
|
Aliases: []string{"asset", "a"},
|
||||||
|
Category: catEntities,
|
||||||
|
Usage: "Manage release assets",
|
||||||
|
Description: "Manage release assets",
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: attachments.RunReleaseAttachmentList,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&attachments.CmdReleaseAttachmentList,
|
||||||
|
&attachments.CmdReleaseAttachmentCreate,
|
||||||
|
&attachments.CmdReleaseAttachmentDelete,
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
71
cmd/attachments/create.go
Normal file
71
cmd/attachments/create.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package attachments
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/cmd/releases"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdReleaseAttachmentCreate represents a sub command of Release Attachments to create a release attachment
|
||||||
|
var CmdReleaseAttachmentCreate = cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Create one or more release attachments",
|
||||||
|
Description: `Create one or more release attachments`,
|
||||||
|
ArgsUsage: "<release-tag> <asset> [<asset>...]",
|
||||||
|
Action: runReleaseAttachmentCreate,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runReleaseAttachmentCreate(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
|
||||||
|
if ctx.Args().Len() < 2 {
|
||||||
|
return fmt.Errorf("no release tag or assets specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := ctx.Args().First()
|
||||||
|
if len(tag) == 0 {
|
||||||
|
return fmt.Errorf("release tag needed to create attachment")
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, asset := range ctx.Args().Slice()[1:] {
|
||||||
|
var file *os.File
|
||||||
|
if file, err = os.Open(asset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Base(asset)
|
||||||
|
|
||||||
|
if _, _, err = ctx.Login.Client().CreateReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, file, filePath); err != nil {
|
||||||
|
file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
89
cmd/attachments/delete.go
Normal file
89
cmd/attachments/delete.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package attachments
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/cmd/releases"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdReleaseAttachmentDelete represents a sub command of Release Attachments to delete a release attachment
|
||||||
|
var CmdReleaseAttachmentDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "Delete one or more release attachments",
|
||||||
|
Description: `Delete one or more release attachments`,
|
||||||
|
ArgsUsage: "<release tag> <attachment name> [<attachment name>...]",
|
||||||
|
Action: runReleaseAttachmentDelete,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Aliases: []string{"y"},
|
||||||
|
Usage: "Confirm deletion (required)",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
|
||||||
|
if ctx.Args().Len() < 2 {
|
||||||
|
return fmt.Errorf("no release tag or attachment names specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := ctx.Args().First()
|
||||||
|
if len(tag) == 0 {
|
||||||
|
return fmt.Errorf("release tag needed to delete attachment")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Bool("confirm") {
|
||||||
|
fmt.Println("Are you sure? Please confirm with -y or --confirm.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, _, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
||||||
|
ListOptions: gitea.ListOptions{Page: -1},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range ctx.Args().Slice()[1:] {
|
||||||
|
var attachment *gitea.Attachment
|
||||||
|
for _, a := range existing {
|
||||||
|
if a.Name == name {
|
||||||
|
attachment = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if attachment == nil {
|
||||||
|
return fmt.Errorf("release does not have attachment named '%s'", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DeleteReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, attachment.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
62
cmd/attachments/list.go
Normal file
62
cmd/attachments/list.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package attachments
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/cmd/releases"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdReleaseAttachmentList represents a sub command of release attachment to list release attachments
|
||||||
|
var CmdReleaseAttachmentList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List Release Attachments",
|
||||||
|
Description: "List Release Attachments",
|
||||||
|
ArgsUsage: "<release-tag>", // command does not accept arguments
|
||||||
|
Action: RunReleaseAttachmentList,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunReleaseAttachmentList list release attachments
|
||||||
|
func RunReleaseAttachmentList(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
|
||||||
|
tag := ctx.Args().First()
|
||||||
|
if len(tag) == 0 {
|
||||||
|
return fmt.Errorf("release tag needed to list attachments")
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments, _, err := ctx.Login.Client().ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.ReleaseAttachmentsList(attachments, ctx.Output)
|
||||||
|
}
|
||||||
39
cmd/branches.go
Normal file
39
cmd/branches.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/branches"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdBranches represents to login a gitea server.
|
||||||
|
var CmdBranches = cli.Command{
|
||||||
|
Name: "branches",
|
||||||
|
Aliases: []string{"branch", "b"},
|
||||||
|
Category: catEntities,
|
||||||
|
Usage: "Consult branches",
|
||||||
|
Description: `Lists branches when called without argument. If a branch is provided, will show it in detail.`,
|
||||||
|
ArgsUsage: "[<branch name>]",
|
||||||
|
Action: runBranches,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&branches.CmdBranchesList,
|
||||||
|
&branches.CmdBranchesProtect,
|
||||||
|
&branches.CmdBranchesUnprotect,
|
||||||
|
&branches.CmdBranchesRename,
|
||||||
|
},
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "comments",
|
||||||
|
Usage: "Whether to display comments (will prompt if not provided & run interactively)",
|
||||||
|
},
|
||||||
|
}, branches.CmdBranchesList.Flags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBranches(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return branches.RunBranchesList(ctx, cmd)
|
||||||
|
}
|
||||||
76
cmd/branches/list.go
Normal file
76
cmd/branches/list.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package branches
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var branchFieldsFlag = flags.FieldsFlag(print.BranchFields, []string{
|
||||||
|
"name", "protected", "user-can-merge", "user-can-push",
|
||||||
|
})
|
||||||
|
|
||||||
|
// CmdBranchesListFlags Flags for command list
|
||||||
|
var CmdBranchesListFlags = append([]cli.Flag{
|
||||||
|
branchFieldsFlag,
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...)
|
||||||
|
|
||||||
|
// CmdBranchesList represents a sub command of branches to list branches
|
||||||
|
var CmdBranchesList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List branches of the repository",
|
||||||
|
Description: `List branches of the repository`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: RunBranchesList,
|
||||||
|
Flags: CmdBranchesListFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunBranchesList list branches
|
||||||
|
func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
owner := ctx.Owner
|
||||||
|
if ctx.IsSet("owner") {
|
||||||
|
owner = ctx.String("owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
var branches []*gitea.Branch
|
||||||
|
var protections []*gitea.BranchProtection
|
||||||
|
branches, _, err = ctx.Login.Client().ListRepoBranches(owner, ctx.Repo, gitea.ListRepoBranchesOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fields, err := branchFieldsFlag.GetValues(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.BranchesList(branches, protections, ctx.Output, fields)
|
||||||
|
}
|
||||||
107
cmd/branches/protect.go
Normal file
107
cmd/branches/protect.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package branches
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdBranchesProtectFlags Flags for command protect/unprotect
|
||||||
|
var CmdBranchesProtectFlags = append([]cli.Flag{
|
||||||
|
branchFieldsFlag,
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...)
|
||||||
|
|
||||||
|
// CmdBranchesProtect represents a sub command of branches to protect a branch
|
||||||
|
var CmdBranchesProtect = cli.Command{
|
||||||
|
Name: "protect",
|
||||||
|
Aliases: []string{"P"},
|
||||||
|
Usage: "Protect branches",
|
||||||
|
Description: `Block actions push/merge on specified branches`,
|
||||||
|
ArgsUsage: "<branch>",
|
||||||
|
Action: RunBranchesProtect,
|
||||||
|
Flags: CmdBranchesProtectFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmdBranchesUnprotect represents a sub command of branches to protect a branch
|
||||||
|
var CmdBranchesUnprotect = cli.Command{
|
||||||
|
Name: "unprotect",
|
||||||
|
Aliases: []string{"U"},
|
||||||
|
Usage: "Unprotect branches",
|
||||||
|
Description: `Suppress existing protections on specified branches`,
|
||||||
|
ArgsUsage: "<branch>",
|
||||||
|
Action: RunBranchesProtect,
|
||||||
|
Flags: CmdBranchesProtectFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunBranchesProtect function to protect/unprotect a list of branches
|
||||||
|
func RunBranchesProtect(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmd.Args().Present() {
|
||||||
|
return fmt.Errorf("must specify at least one branch")
|
||||||
|
}
|
||||||
|
|
||||||
|
owner := ctx.Owner
|
||||||
|
if ctx.IsSet("owner") {
|
||||||
|
owner = ctx.String("owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, branch := range ctx.Args().Slice() {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
command := ctx.Command.Name
|
||||||
|
if command == "protect" {
|
||||||
|
_, _, err = ctx.Login.Client().CreateBranchProtection(owner, ctx.Repo, gitea.CreateBranchProtectionOption{
|
||||||
|
BranchName: branch,
|
||||||
|
RuleName: "",
|
||||||
|
EnablePush: false,
|
||||||
|
EnablePushWhitelist: false,
|
||||||
|
PushWhitelistUsernames: []string{},
|
||||||
|
PushWhitelistTeams: []string{},
|
||||||
|
PushWhitelistDeployKeys: false,
|
||||||
|
EnableMergeWhitelist: false,
|
||||||
|
MergeWhitelistUsernames: []string{},
|
||||||
|
MergeWhitelistTeams: []string{},
|
||||||
|
EnableStatusCheck: false,
|
||||||
|
StatusCheckContexts: []string{},
|
||||||
|
RequiredApprovals: 1,
|
||||||
|
EnableApprovalsWhitelist: false,
|
||||||
|
ApprovalsWhitelistUsernames: []string{},
|
||||||
|
ApprovalsWhitelistTeams: []string{},
|
||||||
|
BlockOnRejectedReviews: false,
|
||||||
|
BlockOnOfficialReviewRequests: false,
|
||||||
|
BlockOnOutdatedBranch: false,
|
||||||
|
DismissStaleApprovals: false,
|
||||||
|
RequireSignedCommits: false,
|
||||||
|
ProtectedFilePatterns: "",
|
||||||
|
UnprotectedFilePatterns: "",
|
||||||
|
})
|
||||||
|
} else if command == "unprotect" {
|
||||||
|
_, err = ctx.Login.Client().DeleteBranchProtection(owner, ctx.Repo, branch)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("command %s is not supported", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
78
cmd/branches/rename.go
Normal file
78
cmd/branches/rename.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package branches
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdBranchesRenameFlags Flags for command rename
|
||||||
|
var CmdBranchesRenameFlags = append([]cli.Flag{
|
||||||
|
branchFieldsFlag,
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...)
|
||||||
|
|
||||||
|
// CmdBranchesRename represents a sub command of branches to rename a branch
|
||||||
|
var CmdBranchesRename = cli.Command{
|
||||||
|
Name: "rename",
|
||||||
|
Aliases: []string{"rn"},
|
||||||
|
Usage: "Rename a branch",
|
||||||
|
Description: `Rename a branch in a repository`,
|
||||||
|
ArgsUsage: "<old_branch_name> <new_branch_name>",
|
||||||
|
Action: RunBranchesRename,
|
||||||
|
Flags: CmdBranchesRenameFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunBranchesRename function to rename a branch
|
||||||
|
func RunBranchesRename(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateRenameArgs(ctx.Args().Slice()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldBranchName := ctx.Args().Get(0)
|
||||||
|
newBranchName := ctx.Args().Get(1)
|
||||||
|
|
||||||
|
owner := ctx.Owner
|
||||||
|
if ctx.IsSet("owner") {
|
||||||
|
owner = ctx.String("owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
successful, _, err := ctx.Login.Client().RenameRepoBranch(owner, ctx.Repo, oldBranchName, gitea.RenameRepoBranchOption{
|
||||||
|
Name: newBranchName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to rename branch: %w", err)
|
||||||
|
}
|
||||||
|
if !successful {
|
||||||
|
return fmt.Errorf("failed to rename branch")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully renamed branch '%s' to '%s'\n", oldBranchName, newBranchName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRenameArgs validates arguments for the rename command
|
||||||
|
func ValidateRenameArgs(args []string) error {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("must specify exactly two arguments: <old_branch_name> <new_branch_name>")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
46
cmd/branches/rename_test.go
Normal file
46
cmd/branches/rename_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package branches
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBranchesRenameArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid args",
|
||||||
|
args: []string{"main", "develop"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing both args",
|
||||||
|
args: []string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing new branch name",
|
||||||
|
args: []string{"main"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many args",
|
||||||
|
args: []string{"main", "develop", "extra"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ValidateRenameArgs(tt.args)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ValidateRenameArgs() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
11
cmd/categories.go
Normal file
11
cmd/categories.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
var (
|
||||||
|
catSetup = "SETUP"
|
||||||
|
catEntities = "ENTITIES"
|
||||||
|
catHelpers = "HELPERS"
|
||||||
|
catMisc = "MISCELLANEOUS"
|
||||||
|
)
|
||||||
100
cmd/clone.go
Normal file
100
cmd/clone.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/debug"
|
||||||
|
"code.gitea.io/tea/modules/git"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdRepoClone represents a sub command of repos to create a local copy
|
||||||
|
var CmdRepoClone = cli.Command{
|
||||||
|
Name: "clone",
|
||||||
|
Aliases: []string{"C"},
|
||||||
|
Usage: "Clone a repository locally",
|
||||||
|
Description: `Clone a repository locally, without a local git installation required.
|
||||||
|
The repo slug can be specified in different formats:
|
||||||
|
gitea/tea
|
||||||
|
tea
|
||||||
|
gitea.com/gitea/tea
|
||||||
|
git@gitea.com:gitea/tea
|
||||||
|
https://gitea.com/gitea/tea
|
||||||
|
ssh://gitea.com:22/gitea/tea
|
||||||
|
When a host is specified in the repo-slug, it will override the login specified with --login.
|
||||||
|
`,
|
||||||
|
Category: catHelpers,
|
||||||
|
Action: runRepoClone,
|
||||||
|
ArgsUsage: "<repo-slug> [target dir]",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "depth",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
Usage: "num commits to fetch, defaults to all",
|
||||||
|
},
|
||||||
|
&flags.LoginFlag,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
teaCmd, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := teaCmd.Args()
|
||||||
|
if args.Len() < 1 {
|
||||||
|
return cli.ShowCommandHelp(ctx, cmd, "clone")
|
||||||
|
}
|
||||||
|
dir := args.Get(1)
|
||||||
|
|
||||||
|
var (
|
||||||
|
login *config.Login = teaCmd.Login
|
||||||
|
owner string
|
||||||
|
repo string
|
||||||
|
)
|
||||||
|
|
||||||
|
// parse first arg as repo specifier
|
||||||
|
repoSlug := args.Get(0)
|
||||||
|
url, err := git.ParseURL(repoSlug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Printf("Cloning repository %s into %s", url.String(), dir)
|
||||||
|
|
||||||
|
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
|
||||||
|
if url.Host != "" {
|
||||||
|
var lookupErr error
|
||||||
|
login, lookupErr = config.GetLoginByHost(url.Host)
|
||||||
|
if lookupErr != nil {
|
||||||
|
return lookupErr
|
||||||
|
}
|
||||||
|
if login == nil {
|
||||||
|
return fmt.Errorf("no login configured matching host '%s', run 'tea login add' first", url.Host)
|
||||||
|
}
|
||||||
|
debug.Printf("Matched login '%s' for host '%s'", login.Name, url.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = task.RepoClone(
|
||||||
|
dir,
|
||||||
|
login,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
interact.PromptPassword,
|
||||||
|
teaCmd.Int("depth"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
107
cmd/cmd.go
Normal file
107
cmd/cmd.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Tea is command line tool for Gitea.
|
||||||
|
package cmd // import "code.gitea.io/tea"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/version"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App creates and returns a tea Command with all subcommands set
|
||||||
|
// it was separated from main so docs can be generated for it
|
||||||
|
func App() *cli.Command {
|
||||||
|
// make parsing tea --version easier, by printing /just/ the version string
|
||||||
|
cli.VersionPrinter = func(c *cli.Command) { fmt.Fprintln(c.Writer, c.Version) }
|
||||||
|
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "tea",
|
||||||
|
Usage: "command line tool to interact with Gitea",
|
||||||
|
Description: appDescription,
|
||||||
|
CustomHelpTemplate: helpTemplate,
|
||||||
|
Version: version.Format(),
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&CmdLogin,
|
||||||
|
&CmdLogout,
|
||||||
|
&CmdWhoami,
|
||||||
|
|
||||||
|
&CmdIssues,
|
||||||
|
&CmdPulls,
|
||||||
|
&CmdLabels,
|
||||||
|
&CmdMilestones,
|
||||||
|
&CmdReleases,
|
||||||
|
&CmdTrackedTimes,
|
||||||
|
&CmdOrgs,
|
||||||
|
&CmdRepos,
|
||||||
|
&CmdBranches,
|
||||||
|
&CmdActions,
|
||||||
|
&CmdWebhooks,
|
||||||
|
&CmdAddComment,
|
||||||
|
|
||||||
|
&CmdOpen,
|
||||||
|
&CmdNotifications,
|
||||||
|
&CmdRepoClone,
|
||||||
|
|
||||||
|
&CmdAdmin,
|
||||||
|
|
||||||
|
&CmdApi,
|
||||||
|
&CmdGenerateManPage,
|
||||||
|
},
|
||||||
|
EnableShellCompletion: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var appDescription = `tea is a productivity helper for Gitea. It can be used to manage most entities on
|
||||||
|
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
|
||||||
|
|
||||||
|
tea tries to make use of context provided by the repository in $PWD if available.
|
||||||
|
tea works best in a upstream/fork workflow, when the local main branch tracks the
|
||||||
|
upstream repo. tea assumes that local git state is published on the remote before
|
||||||
|
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
|
||||||
|
`
|
||||||
|
|
||||||
|
var helpTemplate = fmt.Sprintf("\033[1m%s\033[0m", `
|
||||||
|
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}`) + `
|
||||||
|
{{if .Version}}{{if not .HideVersion}}version {{.Version}}{{end}}{{end}}
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .Commands}} command [subcommand] [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
{{.Description | nindent 3 | trim}}{{end}}{{if .VisibleCommands}}
|
||||||
|
|
||||||
|
COMMANDS{{range .VisibleCategories}}{{if .Name}}
|
||||||
|
{{.Name}}:{{range .VisibleCommands}}
|
||||||
|
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
|
||||||
|
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
{{range $index, $option := .VisibleFlags}}{{if $index}}
|
||||||
|
{{end}}{{$option}}{{end}}{{end}}
|
||||||
|
|
||||||
|
EXAMPLES
|
||||||
|
tea login add # add a login once to get started
|
||||||
|
|
||||||
|
tea pulls # list open pulls for the repo in $PWD
|
||||||
|
tea pulls --repo $HOME/foo # list open pulls for the repo in $HOME/foo
|
||||||
|
tea pulls --remote upstream # list open pulls for the repo pointed at by
|
||||||
|
# your local "upstream" git remote
|
||||||
|
# list open pulls for any gitea repo at the given login instance
|
||||||
|
tea pulls --repo gitea/tea --login gitea.com
|
||||||
|
|
||||||
|
tea milestone issues 0.7.0 # view open issues for milestone '0.7.0'
|
||||||
|
tea issue 189 # view contents of issue 189
|
||||||
|
tea open 189 # open web ui for issue 189
|
||||||
|
tea open milestones # open web ui for milestones
|
||||||
|
|
||||||
|
# send gitea desktop notifications every 5 minutes (bash + libnotify)
|
||||||
|
while :; do tea notifications --mine -o simple | xargs -i notify-send {}; sleep 300; done
|
||||||
|
|
||||||
|
ABOUT
|
||||||
|
Written & maintained by The Gitea Authors.
|
||||||
|
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea.
|
||||||
|
More info about Gitea itself on https://about.gitea.com.
|
||||||
|
`
|
||||||
95
cmd/comment.go
Normal file
95
cmd/comment.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/theme"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"charm.land/huh/v2"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdAddComment is the main command to operate with notifications
|
||||||
|
var CmdAddComment = cli.Command{
|
||||||
|
Name: "comment",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Category: catEntities,
|
||||||
|
Usage: "Add a comment to an issue / pr",
|
||||||
|
Description: "Add a comment to an issue / pr",
|
||||||
|
ArgsUsage: "<issue / pr index> [<comment body>]",
|
||||||
|
Action: runAddComment,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAddComment(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := ctx.Args()
|
||||||
|
if args.Len() == 0 {
|
||||||
|
return fmt.Errorf("please specify issue / pr index")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body := strings.Join(ctx.Args().Tail(), " ")
|
||||||
|
if interact.IsStdinPiped() {
|
||||||
|
// custom solution until https://github.com/AlecAivazis/survey/issues/328 is fixed
|
||||||
|
if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(bodyStdin) != 0 {
|
||||||
|
body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
|
||||||
|
}
|
||||||
|
} else if len(body) == 0 {
|
||||||
|
if err := huh.NewForm(
|
||||||
|
huh.NewGroup(
|
||||||
|
huh.NewText().
|
||||||
|
Title("Comment(markdown):").
|
||||||
|
ExternalEditor(config.GetPreferences().Editor).
|
||||||
|
EditorExtension("md").
|
||||||
|
Value(&body),
|
||||||
|
),
|
||||||
|
).WithTheme(theme.GetTheme()).
|
||||||
|
Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
return errors.New("no comment content provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
comment, _, err := client.CreateIssueComment(ctx.Owner, ctx.Repo, idx, gitea.CreateIssueCommentOption{
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
print.Comment(comment)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
255
cmd/config.go
255
cmd/config.go
@@ -1,255 +0,0 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
|
||||||
"code.gitea.io/tea/modules/git"
|
|
||||||
"code.gitea.io/tea/modules/utils"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Login represents a login to a gitea server, you even could add multiple logins for one gitea server
|
|
||||||
type Login struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
URL string `yaml:"url"`
|
|
||||||
Token string `yaml:"token"`
|
|
||||||
Active bool `yaml:"active"`
|
|
||||||
SSHHost string `yaml:"ssh_host"`
|
|
||||||
// optional path to the private key
|
|
||||||
SSHKey string `yaml:"ssh_key"`
|
|
||||||
Insecure bool `yaml:"insecure"`
|
|
||||||
// optional gitea username
|
|
||||||
User string `yaml:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns a client to operate Gitea API
|
|
||||||
func (l *Login) Client() *gitea.Client {
|
|
||||||
client := gitea.NewClient(l.URL, l.Token)
|
|
||||||
if l.Insecure {
|
|
||||||
cookieJar, _ := cookiejar.New(nil)
|
|
||||||
|
|
||||||
client.SetHTTPClient(&http.Client{
|
|
||||||
Jar: cookieJar,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSSHHost returns SSH host name
|
|
||||||
func (l *Login) GetSSHHost() string {
|
|
||||||
if l.SSHHost != "" {
|
|
||||||
return l.SSHHost
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(l.URL)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.Hostname()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config reprensents local configurations
|
|
||||||
type Config struct {
|
|
||||||
Logins []Login `yaml:"logins"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
config Config
|
|
||||||
yamlConfigPath string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
homeDir, err := utils.Home()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Retrieve home dir failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := filepath.Join(homeDir, ".tea")
|
|
||||||
err = os.MkdirAll(dir, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Init tea config dir " + dir + " failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
yamlConfigPath = filepath.Join(dir, "tea.yml")
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitRepo(repoPath string) (string, string) {
|
|
||||||
p := strings.Split(repoPath, "/")
|
|
||||||
if len(p) >= 2 {
|
|
||||||
return p[0], p[1]
|
|
||||||
}
|
|
||||||
return repoPath, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getActiveLogin() (*Login, error) {
|
|
||||||
if len(config.Logins) == 0 {
|
|
||||||
return nil, errors.New("No available login")
|
|
||||||
}
|
|
||||||
for _, l := range config.Logins {
|
|
||||||
if l.Active {
|
|
||||||
return &l, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &config.Logins[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLoginByName(name string) *Login {
|
|
||||||
for _, l := range config.Logins {
|
|
||||||
if l.Name == name {
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addLogin(login Login) error {
|
|
||||||
for _, l := range config.Logins {
|
|
||||||
if l.Name == login.Name {
|
|
||||||
if l.URL == login.URL && l.Token == login.Token {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New("Login name has already been used")
|
|
||||||
}
|
|
||||||
if l.URL == login.URL && l.Token == login.Token {
|
|
||||||
return errors.New("URL has been added")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(login.URL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.SSHHost == "" {
|
|
||||||
login.SSHHost = u.Hostname()
|
|
||||||
}
|
|
||||||
config.Logins = append(config.Logins, login)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isFileExist(fileName string) (bool, error) {
|
|
||||||
f, err := os.Stat(fileName)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if f.IsDir() {
|
|
||||||
return false, errors.New("A directory with the same name exists")
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig(ymlPath string) error {
|
|
||||||
exist, _ := isFileExist(ymlPath)
|
|
||||||
if exist {
|
|
||||||
Println("Found config file", ymlPath)
|
|
||||||
bs, err := ioutil.ReadFile(ymlPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = yaml.Unmarshal(bs, &config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveConfig(ymlPath string) error {
|
|
||||||
bs, err := yaml.Marshal(&config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ioutil.WriteFile(ymlPath, bs, 0660)
|
|
||||||
}
|
|
||||||
|
|
||||||
func curGitRepoPath(path string) (*Login, string, error) {
|
|
||||||
var err error
|
|
||||||
var repo *git.TeaRepo
|
|
||||||
if len(path) == 0 {
|
|
||||||
repo, err = git.RepoForWorkdir()
|
|
||||||
} else {
|
|
||||||
repo, err = git.RepoFromPath(path)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
gitConfig, err := repo.Config()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no remote
|
|
||||||
if len(gitConfig.Remotes) == 0 {
|
|
||||||
return nil, "", errors.New("No remote(s) found in this Git repository")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if only one remote exists
|
|
||||||
if len(gitConfig.Remotes) >= 1 && len(remoteValue) == 0 {
|
|
||||||
for remote := range gitConfig.Remotes {
|
|
||||||
remoteValue = remote
|
|
||||||
}
|
|
||||||
if len(gitConfig.Remotes) > 1 {
|
|
||||||
// if master branch is present, use it as the default remote
|
|
||||||
masterBranch, ok := gitConfig.Branches["master"]
|
|
||||||
if ok {
|
|
||||||
if len(masterBranch.Remote) > 0 {
|
|
||||||
remoteValue = masterBranch.Remote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteConfig, ok := gitConfig.Remotes[remoteValue]
|
|
||||||
if !ok || remoteConfig == nil {
|
|
||||||
return nil, "", errors.New("Remote " + remoteValue + " not found in this Git repository")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range config.Logins {
|
|
||||||
for _, u := range remoteConfig.URLs {
|
|
||||||
p, err := git.ParseURL(strings.TrimSpace(u))
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error())
|
|
||||||
}
|
|
||||||
if strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https") {
|
|
||||||
if strings.HasPrefix(u, l.URL) {
|
|
||||||
ps := strings.Split(p.Path, "/")
|
|
||||||
path := strings.Join(ps[len(ps)-2:], "/")
|
|
||||||
return &l, strings.TrimSuffix(path, ".git"), nil
|
|
||||||
}
|
|
||||||
} else if strings.EqualFold(p.Scheme, "ssh") {
|
|
||||||
if l.GetSSHHost() == strings.Split(p.Host, ":")[0] {
|
|
||||||
return &l, strings.TrimLeft(strings.TrimSuffix(p.Path, ".git"), "/"), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, "", errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
|
|
||||||
}
|
|
||||||
93
cmd/detail_json.go
Normal file
93
cmd/detail_json.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type detailLabelData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type detailCommentData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type detailReviewData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Reviewer string `json:"reviewer"`
|
||||||
|
State gitea.ReviewStateType `json:"state"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDetailLabels(labels []*gitea.Label) []detailLabelData {
|
||||||
|
labelSlice := make([]detailLabelData, 0, len(labels))
|
||||||
|
for _, label := range labels {
|
||||||
|
labelSlice = append(labelSlice, detailLabelData{
|
||||||
|
Name: label.Name,
|
||||||
|
Color: label.Color,
|
||||||
|
Description: label.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return labelSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDetailAssignees(assignees []*gitea.User) []string {
|
||||||
|
assigneeSlice := make([]string, 0, len(assignees))
|
||||||
|
for _, assignee := range assignees {
|
||||||
|
assigneeSlice = append(assigneeSlice, username(assignee))
|
||||||
|
}
|
||||||
|
return assigneeSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDetailComments(comments []*gitea.Comment) []detailCommentData {
|
||||||
|
commentSlice := make([]detailCommentData, 0, len(comments))
|
||||||
|
for _, comment := range comments {
|
||||||
|
commentSlice = append(commentSlice, detailCommentData{
|
||||||
|
ID: comment.ID,
|
||||||
|
Author: username(comment.Poster),
|
||||||
|
Body: comment.Body,
|
||||||
|
Created: comment.Created,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return commentSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDetailReviews(reviews []*gitea.PullReview) []detailReviewData {
|
||||||
|
reviewSlice := make([]detailReviewData, 0, len(reviews))
|
||||||
|
for _, review := range reviews {
|
||||||
|
reviewSlice = append(reviewSlice, detailReviewData{
|
||||||
|
ID: review.ID,
|
||||||
|
Reviewer: username(review.Reviewer),
|
||||||
|
State: review.State,
|
||||||
|
Body: review.Body,
|
||||||
|
Created: review.Submitted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return reviewSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func username(user *gitea.User) string {
|
||||||
|
if user == nil {
|
||||||
|
return "ghost"
|
||||||
|
}
|
||||||
|
return user.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeIndentedJSON(w io.Writer, data any) error {
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
encoder.SetIndent("", "\t")
|
||||||
|
return encoder.Encode(data)
|
||||||
|
}
|
||||||
126
cmd/flags.go
126
cmd/flags.go
@@ -1,126 +0,0 @@
|
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// create global variables for global Flags to simplify
|
|
||||||
// access to the options without requiring cli.Context
|
|
||||||
var (
|
|
||||||
loginValue string
|
|
||||||
repoValue string
|
|
||||||
outputValue string
|
|
||||||
remoteValue string
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginFlag provides flag to specify tea login profile
|
|
||||||
var LoginFlag = cli.StringFlag{
|
|
||||||
Name: "login",
|
|
||||||
Aliases: []string{"l"},
|
|
||||||
Usage: "Use a different Gitea login. Optional",
|
|
||||||
Destination: &loginValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepoFlag provides flag to specify repository
|
|
||||||
var RepoFlag = cli.StringFlag{
|
|
||||||
Name: "repo",
|
|
||||||
Aliases: []string{"r"},
|
|
||||||
Usage: "Repository to interact with. Optional",
|
|
||||||
Destination: &repoValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteFlag provides flag to specify remote repository
|
|
||||||
var RemoteFlag = cli.StringFlag{
|
|
||||||
Name: "remote",
|
|
||||||
Aliases: []string{"R"},
|
|
||||||
Usage: "Discover Gitea login from remote. Optional",
|
|
||||||
Destination: &remoteValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutputFlag provides flag to specify output type
|
|
||||||
var OutputFlag = cli.StringFlag{
|
|
||||||
Name: "output",
|
|
||||||
Aliases: []string{"o"},
|
|
||||||
Usage: "Output format. (csv, simple, table, tsv, yaml)",
|
|
||||||
Destination: &outputValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginOutputFlags defines login and output flags that should
|
|
||||||
// added to all subcommands and appended to the flags of the
|
|
||||||
// subcommand to work around issue and provide --login and --output:
|
|
||||||
// https://github.com/urfave/cli/issues/585
|
|
||||||
var LoginOutputFlags = []cli.Flag{
|
|
||||||
&LoginFlag,
|
|
||||||
&OutputFlag,
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginRepoFlags defines login and repo flags that should
|
|
||||||
// be used for all subcommands and appended to the flags of
|
|
||||||
// the subcommand to work around issue and provide --login and --repo:
|
|
||||||
// https://github.com/urfave/cli/issues/585
|
|
||||||
var LoginRepoFlags = []cli.Flag{
|
|
||||||
&LoginFlag,
|
|
||||||
&RepoFlag,
|
|
||||||
&RemoteFlag,
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllDefaultFlags defines flags that should be available
|
|
||||||
// for all subcommands working with dedicated repositories
|
|
||||||
// to work around issue and provide --login, --repo and --output:
|
|
||||||
// https://github.com/urfave/cli/issues/585
|
|
||||||
var AllDefaultFlags = append([]cli.Flag{
|
|
||||||
&RepoFlag,
|
|
||||||
&RemoteFlag,
|
|
||||||
}, LoginOutputFlags...)
|
|
||||||
|
|
||||||
// initCommand returns repository and *Login based on flags
|
|
||||||
func initCommand() (*Login, string, string) {
|
|
||||||
err := loadConfig(yamlConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("load config file failed ", yamlConfigPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
login, repoPath, err := curGitRepoPath(repoValue)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if loginValue != "" {
|
|
||||||
login = getLoginByName(loginValue)
|
|
||||||
if login == nil {
|
|
||||||
log.Fatal("Login name " + loginValue + " does not exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
owner, repo := splitRepo(repoPath)
|
|
||||||
return login, owner, repo
|
|
||||||
}
|
|
||||||
|
|
||||||
// initCommandLoginOnly return *Login based on flags
|
|
||||||
func initCommandLoginOnly() *Login {
|
|
||||||
err := loadConfig(yamlConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("load config file failed ", yamlConfigPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var login *Login
|
|
||||||
if loginValue == "" {
|
|
||||||
login, err = getActiveLogin()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
login = getLoginByName(loginValue)
|
|
||||||
if login == nil {
|
|
||||||
log.Fatal("Login name " + loginValue + " does not exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return login
|
|
||||||
}
|
|
||||||
52
cmd/flags/csvflag.go
Normal file
52
cmd/flags/csvflag.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CsvFlag is a wrapper around cli.StringFlag, with an added GetValues() method
|
||||||
|
// to retrieve comma separated string values as a slice.
|
||||||
|
type CsvFlag struct {
|
||||||
|
cli.StringFlag
|
||||||
|
AvailableFields []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCsvFlag creates a CsvFlag, while setting its usage string and default values
|
||||||
|
func NewCsvFlag(name, usage string, aliases, availableValues, defaults []string) *CsvFlag {
|
||||||
|
var availableDesc string
|
||||||
|
if len(availableValues) != 0 {
|
||||||
|
availableDesc = " Available values:"
|
||||||
|
}
|
||||||
|
return &CsvFlag{
|
||||||
|
AvailableFields: availableValues,
|
||||||
|
StringFlag: cli.StringFlag{
|
||||||
|
Name: name,
|
||||||
|
Aliases: aliases,
|
||||||
|
Value: strings.Join(defaults, ","),
|
||||||
|
Usage: fmt.Sprintf(`Comma-separated list of %s.%s
|
||||||
|
%s
|
||||||
|
`, usage, availableDesc, strings.Join(availableValues, ",")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues returns the value of the flag, parsed as a commaseparated list
|
||||||
|
func (f CsvFlag) GetValues(cmd *cli.Command) ([]string, error) {
|
||||||
|
val := cmd.String(f.Name)
|
||||||
|
selection := strings.Split(val, ",")
|
||||||
|
if f.AvailableFields != nil && val != "" {
|
||||||
|
for _, field := range selection {
|
||||||
|
if !utils.Contains(f.AvailableFields, field) {
|
||||||
|
return nil, fmt.Errorf("invalid field '%s'", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selection, nil
|
||||||
|
}
|
||||||
190
cmd/flags/generic.go
Normal file
190
cmd/flags/generic.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginFlag provides flag to specify tea login profile
|
||||||
|
var LoginFlag = cli.StringFlag{
|
||||||
|
Name: "login",
|
||||||
|
Aliases: []string{"l"},
|
||||||
|
Usage: "Use a different Gitea Login. Optional",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepoFlag provides flag to specify repository
|
||||||
|
var RepoFlag = cli.StringFlag{
|
||||||
|
Name: "repo",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "Override local repository path or gitea repository slug to interact with. Optional",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteFlag provides flag to specify remote repository
|
||||||
|
var RemoteFlag = cli.StringFlag{
|
||||||
|
Name: "remote",
|
||||||
|
Aliases: []string{"R"},
|
||||||
|
Usage: "Discover Gitea login from remote. Optional",
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputFlag provides flag to specify output type
|
||||||
|
var OutputFlag = cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "Output format. (simple, table, csv, tsv, yaml, json)",
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrPage indicates that the provided page value is invalid (less than -1 or equal to 0).
|
||||||
|
ErrPage = errors.New("page cannot be smaller than 1")
|
||||||
|
// ErrLimit indicates that the provided limit value is invalid (negative).
|
||||||
|
ErrLimit = errors.New("limit cannot be negative")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPageValue = 1
|
||||||
|
defaultLimitValue = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetListOptions returns list options derived from the active command.
|
||||||
|
func GetListOptions(cmd *cli.Command) gitea.ListOptions {
|
||||||
|
page := cmd.Int("page")
|
||||||
|
if page == 0 {
|
||||||
|
page = defaultPageValue
|
||||||
|
}
|
||||||
|
|
||||||
|
pageSize := cmd.Int("limit")
|
||||||
|
if pageSize == 0 {
|
||||||
|
pageSize = defaultLimitValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return gitea.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationFlags provides all pagination related flags
|
||||||
|
var PaginationFlags = []cli.Flag{
|
||||||
|
&PaginationPageFlag,
|
||||||
|
&PaginationLimitFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationPageFlag provides flag for pagination options
|
||||||
|
var PaginationPageFlag = cli.IntFlag{
|
||||||
|
Name: "page",
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
Usage: "specify page",
|
||||||
|
Value: defaultPageValue,
|
||||||
|
Validator: func(i int) error {
|
||||||
|
if i < 1 && i != -1 {
|
||||||
|
return ErrPage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationLimitFlag provides flag for pagination options
|
||||||
|
var PaginationLimitFlag = cli.IntFlag{
|
||||||
|
Name: "limit",
|
||||||
|
Aliases: []string{"lm"},
|
||||||
|
Usage: "specify limit of items per page",
|
||||||
|
Value: defaultLimitValue,
|
||||||
|
Validator: func(i int) error {
|
||||||
|
if i < 0 {
|
||||||
|
return ErrLimit
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginOutputFlags defines login and output flags that should
|
||||||
|
// added to all subcommands and appended to the flags of the
|
||||||
|
// subcommand to work around issue and provide --login and --output:
|
||||||
|
// https://github.com/urfave/cli/issues/585
|
||||||
|
var LoginOutputFlags = []cli.Flag{
|
||||||
|
&LoginFlag,
|
||||||
|
&OutputFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRepoFlags defines login and repo flags that should
|
||||||
|
// be used for all subcommands and appended to the flags of
|
||||||
|
// the subcommand to work around issue and provide --login and --repo:
|
||||||
|
// https://github.com/urfave/cli/issues/585
|
||||||
|
var LoginRepoFlags = []cli.Flag{
|
||||||
|
&LoginFlag,
|
||||||
|
&RepoFlag,
|
||||||
|
&RemoteFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllDefaultFlags defines flags that should be available
|
||||||
|
// for all subcommands working with dedicated repositories
|
||||||
|
// to work around issue and provide --login, --repo and --output:
|
||||||
|
// https://github.com/urfave/cli/issues/585
|
||||||
|
var AllDefaultFlags = append([]cli.Flag{
|
||||||
|
&RepoFlag,
|
||||||
|
&RemoteFlag,
|
||||||
|
}, LoginOutputFlags...)
|
||||||
|
|
||||||
|
// NotificationFlags defines flags that should be available on notifications.
|
||||||
|
var NotificationFlags = append([]cli.Flag{
|
||||||
|
NotificationStateFlag,
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "mine",
|
||||||
|
Aliases: []string{"m"},
|
||||||
|
Usage: "Show notifications across all your repositories instead of the current repository only",
|
||||||
|
},
|
||||||
|
&PaginationPageFlag,
|
||||||
|
&PaginationLimitFlag,
|
||||||
|
}, AllDefaultFlags...)
|
||||||
|
|
||||||
|
// NotificationStateFlag is a csv flag applied to all notification subcommands as filter
|
||||||
|
var NotificationStateFlag = NewCsvFlag(
|
||||||
|
"states",
|
||||||
|
"notification states to filter by",
|
||||||
|
[]string{"s"},
|
||||||
|
[]string{"pinned", "unread", "read"},
|
||||||
|
[]string{"unread", "pinned"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// FieldsFlag generates a flag selecting printable fields.
|
||||||
|
// To retrieve the value, use f.GetValues()
|
||||||
|
func FieldsFlag(availableFields, defaultFields []string) *CsvFlag {
|
||||||
|
return NewCsvFlag("fields", "fields to print", []string{"f"}, availableFields, defaultFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseState parses a state string and returns the corresponding gitea.StateType
|
||||||
|
func ParseState(stateStr string) (gitea.StateType, error) {
|
||||||
|
switch stateStr {
|
||||||
|
case "all":
|
||||||
|
return gitea.StateAll, nil
|
||||||
|
case "", "open":
|
||||||
|
return gitea.StateOpen, nil
|
||||||
|
case "closed":
|
||||||
|
return gitea.StateClosed, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown state '%s'", stateStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseIssueKind parses a kind string and returns the corresponding gitea.IssueType.
|
||||||
|
// If kindStr is empty, returns the provided defaultKind.
|
||||||
|
func ParseIssueKind(kindStr string, defaultKind gitea.IssueType) (gitea.IssueType, error) {
|
||||||
|
switch kindStr {
|
||||||
|
case "":
|
||||||
|
return defaultKind, nil
|
||||||
|
case "all":
|
||||||
|
return gitea.IssueTypeAll, nil
|
||||||
|
case "issue", "issues":
|
||||||
|
return gitea.IssueTypeIssue, nil
|
||||||
|
case "pull", "pulls", "pr":
|
||||||
|
return gitea.IssueTypePull, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown kind '%s'", kindStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
152
cmd/flags/generic_test.go
Normal file
152
cmd/flags/generic_test.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPaginationFlags(t *testing.T) {
|
||||||
|
var (
|
||||||
|
defaultPage = PaginationPageFlag.Value
|
||||||
|
defaultLimit = PaginationLimitFlag.Value
|
||||||
|
)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
expectedPage int
|
||||||
|
expectedLimit int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no flags",
|
||||||
|
args: []string{"test"},
|
||||||
|
expectedPage: defaultPage,
|
||||||
|
expectedLimit: defaultLimit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only paging",
|
||||||
|
args: []string{"test", "--page", "5"},
|
||||||
|
expectedPage: 5,
|
||||||
|
expectedLimit: defaultLimit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only limit",
|
||||||
|
args: []string{"test", "--limit", "10"},
|
||||||
|
expectedPage: defaultPage,
|
||||||
|
expectedLimit: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only limit",
|
||||||
|
args: []string{"test", "--limit", "10"},
|
||||||
|
expectedPage: defaultPage,
|
||||||
|
expectedLimit: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both flags",
|
||||||
|
args: []string{"test", "--page", "2", "--limit", "20"},
|
||||||
|
expectedPage: 2,
|
||||||
|
expectedLimit: 20,
|
||||||
|
},
|
||||||
|
{ // TODO: Should no paging be applied as -1 or a separate flag? It's not obvious that page=-1 turns off paging and limit is ignored
|
||||||
|
name: "no paging",
|
||||||
|
args: []string{"test", "--limit", "20", "--page", "-1"},
|
||||||
|
expectedPage: -1,
|
||||||
|
expectedLimit: 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cmd := cli.Command{
|
||||||
|
Name: "test-paging",
|
||||||
|
Action: func(_ context.Context, cmd *cli.Command) error {
|
||||||
|
assert.Equal(t, tc.expectedPage, cmd.Int("page"))
|
||||||
|
assert.Equal(t, tc.expectedLimit, cmd.Int("limit"))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Flags: PaginationFlags,
|
||||||
|
}
|
||||||
|
err := cmd.Run(context.Background(), tc.args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPaginationFailures(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "negative limit",
|
||||||
|
args: []string{"test", "--limit", "-10"},
|
||||||
|
expectedError: ErrLimit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative paging",
|
||||||
|
args: []string{"test", "--page", "-2"},
|
||||||
|
expectedError: ErrPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero paging",
|
||||||
|
args: []string{"test", "--page", "0"},
|
||||||
|
expectedError: ErrPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// urfave does not validate all flags in one pass
|
||||||
|
name: "negative paging and paging",
|
||||||
|
args: []string{"test", "--page", "-2", "--limit", "-10"},
|
||||||
|
expectedError: ErrPage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
cmd := cli.Command{
|
||||||
|
Name: "test-paging",
|
||||||
|
Flags: PaginationFlags,
|
||||||
|
Writer: io.Discard,
|
||||||
|
ErrWriter: io.Discard,
|
||||||
|
}
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := cmd.Run(context.Background(), tc.args)
|
||||||
|
require.ErrorContains(t, err, tc.expectedError.Error())
|
||||||
|
// require.ErrorIs(t, err, tc.expectedError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetListOptionsDoesNotLeakBetweenCommands(t *testing.T) {
|
||||||
|
var results []gitea.ListOptions
|
||||||
|
|
||||||
|
run := func(args []string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cmd := cli.Command{
|
||||||
|
Name: "test-paging",
|
||||||
|
Action: func(_ context.Context, cmd *cli.Command) error {
|
||||||
|
results = append(results, GetListOptions(cmd))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Flags: PaginationFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cmd.Run(context.Background(), args))
|
||||||
|
}
|
||||||
|
|
||||||
|
run([]string{"test", "--page", "5", "--limit", "10"})
|
||||||
|
run([]string{"test"})
|
||||||
|
|
||||||
|
require.Len(t, results, 2)
|
||||||
|
assert.Equal(t, gitea.ListOptions{Page: 5, PageSize: 10}, results[0])
|
||||||
|
assert.Equal(t, gitea.ListOptions{Page: defaultPageValue, PageSize: defaultLimitValue}, results[1])
|
||||||
|
}
|
||||||
238
cmd/flags/issue_pr.go
Normal file
238
cmd/flags/issue_pr.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StateFlag provides flag to specify issue/pr state, defaulting to "open"
|
||||||
|
var StateFlag = cli.StringFlag{
|
||||||
|
Name: "state",
|
||||||
|
Usage: "Filter by state (all|open|closed)",
|
||||||
|
DefaultText: "open",
|
||||||
|
}
|
||||||
|
|
||||||
|
// MilestoneFilterFlag is a CSV flag used to filter issues by milestones
|
||||||
|
var MilestoneFilterFlag = NewCsvFlag(
|
||||||
|
"milestones",
|
||||||
|
"milestones to match issues against",
|
||||||
|
[]string{"m"}, nil, nil)
|
||||||
|
|
||||||
|
// LabelFilterFlag is a CSV flag used to filter issues by labels
|
||||||
|
var LabelFilterFlag = NewCsvFlag(
|
||||||
|
"labels",
|
||||||
|
"labels to match issues against",
|
||||||
|
[]string{"L"}, nil, nil)
|
||||||
|
|
||||||
|
// PRListingFlags defines flags that should be available on pr listing flags.
|
||||||
|
var PRListingFlags = append([]cli.Flag{
|
||||||
|
&StateFlag,
|
||||||
|
&PaginationPageFlag,
|
||||||
|
&PaginationLimitFlag,
|
||||||
|
}, AllDefaultFlags...)
|
||||||
|
|
||||||
|
// IssueListingFlags defines flags that should be available on issue listing flags.
|
||||||
|
var IssueListingFlags = append([]cli.Flag{
|
||||||
|
&StateFlag,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "kind",
|
||||||
|
Aliases: []string{"K"},
|
||||||
|
Usage: "Whether to return `issues`, `pulls`, or `all` (you can use this to apply advanced search filters to PRs)",
|
||||||
|
DefaultText: "issues",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "keyword",
|
||||||
|
Aliases: []string{"k"},
|
||||||
|
Usage: "Filter by search string",
|
||||||
|
},
|
||||||
|
LabelFilterFlag,
|
||||||
|
MilestoneFilterFlag,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "author",
|
||||||
|
Aliases: []string{"A"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "assignee",
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mentions",
|
||||||
|
Aliases: []string{"M"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "owner",
|
||||||
|
Aliases: []string{"org"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "from",
|
||||||
|
Aliases: []string{"F"},
|
||||||
|
Usage: "Filter by activity after this date",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "until",
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
Usage: "Filter by activity before this date",
|
||||||
|
},
|
||||||
|
&PaginationPageFlag,
|
||||||
|
&PaginationLimitFlag,
|
||||||
|
}, AllDefaultFlags...)
|
||||||
|
|
||||||
|
// issuePRFlags defines shared flags between flags IssuePRCreateFlags and IssuePREditFlags
|
||||||
|
var issuePRFlags = append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "title",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "referenced-version",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "commit-hash or tag name to assign",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "milestone",
|
||||||
|
Aliases: []string{"m"},
|
||||||
|
Usage: "Milestone to assign",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "deadline",
|
||||||
|
Aliases: []string{"D"},
|
||||||
|
Usage: "Deadline timestamp to assign",
|
||||||
|
},
|
||||||
|
}, LoginRepoFlags...)
|
||||||
|
|
||||||
|
// IssuePRCreateFlags defines flags for creation of issues and PRs
|
||||||
|
var IssuePRCreateFlags = append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "assignees",
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Usage: "Comma-separated list of usernames to assign",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "labels",
|
||||||
|
Aliases: []string{"L"},
|
||||||
|
Usage: "Comma-separated list of labels to assign",
|
||||||
|
},
|
||||||
|
}, issuePRFlags...)
|
||||||
|
|
||||||
|
// GetIssuePRCreateFlags parses all IssuePREditFlags
|
||||||
|
func GetIssuePRCreateFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
|
||||||
|
opts := gitea.CreateIssueOption{
|
||||||
|
Title: ctx.String("title"),
|
||||||
|
Body: ctx.String("description"),
|
||||||
|
Assignees: strings.Split(ctx.String("assignees"), ","),
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
date := ctx.String("deadline")
|
||||||
|
if date != "" {
|
||||||
|
t, err := dateparse.ParseAny(date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts.Deadline = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
|
||||||
|
labelNames := strings.Split(ctx.String("labels"), ",")
|
||||||
|
if len(labelNames) != 0 {
|
||||||
|
if client == nil {
|
||||||
|
client = ctx.Login.Client()
|
||||||
|
}
|
||||||
|
if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if milestoneName := ctx.String("milestone"); len(milestoneName) != 0 {
|
||||||
|
if client == nil {
|
||||||
|
client = ctx.Login.Client()
|
||||||
|
}
|
||||||
|
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("milestone '%s' not found", milestoneName)
|
||||||
|
}
|
||||||
|
opts.Milestone = ms.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return &opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssuePREditFlags defines flags for editing properties of issues and PRs
|
||||||
|
var IssuePREditFlags = append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "add-assignees",
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Usage: "Comma-separated list of usernames to assign",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "add-labels",
|
||||||
|
Aliases: []string{"L"},
|
||||||
|
Usage: "Comma-separated list of labels to assign. Takes precedence over --remove-labels",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "remove-labels",
|
||||||
|
Usage: "Comma-separated list of labels to remove",
|
||||||
|
},
|
||||||
|
}, issuePRFlags...)
|
||||||
|
|
||||||
|
// GetIssuePREditFlags parses all IssuePREditFlags
|
||||||
|
func GetIssuePREditFlags(ctx *context.TeaContext) (*task.EditIssueOption, error) {
|
||||||
|
opts := task.EditIssueOption{}
|
||||||
|
if ctx.IsSet("title") {
|
||||||
|
val := ctx.String("title")
|
||||||
|
opts.Title = &val
|
||||||
|
}
|
||||||
|
if ctx.IsSet("description") {
|
||||||
|
val := ctx.String("description")
|
||||||
|
opts.Body = &val
|
||||||
|
}
|
||||||
|
if ctx.IsSet("referenced-version") {
|
||||||
|
val := ctx.String("referenced-version")
|
||||||
|
opts.Ref = &val
|
||||||
|
}
|
||||||
|
if ctx.IsSet("milestone") {
|
||||||
|
val := ctx.String("milestone")
|
||||||
|
opts.Milestone = &val
|
||||||
|
}
|
||||||
|
if ctx.IsSet("deadline") {
|
||||||
|
date := ctx.String("deadline")
|
||||||
|
if date == "" {
|
||||||
|
opts.Deadline = &time.Time{}
|
||||||
|
} else {
|
||||||
|
t, err := dateparse.ParseAny(date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts.Deadline = &t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ctx.IsSet("add-assignees") {
|
||||||
|
val := ctx.String("add-assignees")
|
||||||
|
opts.AddAssignees = strings.Split(val, ",")
|
||||||
|
}
|
||||||
|
if ctx.IsSet("add-labels") {
|
||||||
|
val := ctx.String("add-labels")
|
||||||
|
opts.AddLabels = strings.Split(val, ",")
|
||||||
|
}
|
||||||
|
if ctx.IsSet("remove-labels") {
|
||||||
|
val := ctx.String("remove-labels")
|
||||||
|
opts.RemoveLabels = strings.Split(val, ",")
|
||||||
|
}
|
||||||
|
return &opts, nil
|
||||||
|
}
|
||||||
284
cmd/issues.go
284
cmd/issues.go
@@ -1,217 +1,173 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// SPDX-License-Identifier: MIT
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
stdctx "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"time"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/cmd/issues"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type labelData = detailLabelData
|
||||||
|
|
||||||
|
type issueData struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Index int64 `json:"index"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
State gitea.StateType `json:"state"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Labels []labelData `json:"labels"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Assignees []string `json:"assignees"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ClosedAt *time.Time `json:"closedAt"`
|
||||||
|
Comments []commentData `json:"comments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type issueDetailClient interface {
|
||||||
|
GetIssue(owner, repo string, index int64) (*gitea.Issue, *gitea.Response, error)
|
||||||
|
GetIssueReactions(owner, repo string, index int64) ([]*gitea.Reaction, *gitea.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type issueCommentClient interface {
|
||||||
|
ListIssueComments(owner, repo string, index int64, opt gitea.ListIssueCommentOptions) ([]*gitea.Comment, *gitea.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commentData = detailCommentData
|
||||||
|
|
||||||
// CmdIssues represents to login a gitea server.
|
// CmdIssues represents to login a gitea server.
|
||||||
var CmdIssues = cli.Command{
|
var CmdIssues = cli.Command{
|
||||||
Name: "issues",
|
Name: "issues",
|
||||||
Usage: "List and create issues",
|
Aliases: []string{"issue", "i"},
|
||||||
Description: `List and create issues`,
|
Category: catEntities,
|
||||||
|
Usage: "List, create and update issues",
|
||||||
|
Description: `Lists issues when called without argument. If issue index is provided, will show it in detail.`,
|
||||||
ArgsUsage: "[<issue index>]",
|
ArgsUsage: "[<issue index>]",
|
||||||
Action: runIssues,
|
Action: runIssues,
|
||||||
Subcommands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
&CmdIssuesList,
|
&issues.CmdIssuesList,
|
||||||
&CmdIssuesCreate,
|
&issues.CmdIssuesCreate,
|
||||||
&CmdIssuesReopen,
|
&issues.CmdIssuesEdit,
|
||||||
&CmdIssuesClose,
|
&issues.CmdIssuesReopen,
|
||||||
|
&issues.CmdIssuesClose,
|
||||||
},
|
},
|
||||||
Flags: AllDefaultFlags,
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdIssuesList represents a sub command of issues to list issues
|
|
||||||
var CmdIssuesList = cli.Command{
|
|
||||||
Name: "ls",
|
|
||||||
Usage: "List issues of the repository",
|
|
||||||
Description: `List issues of the repository`,
|
|
||||||
Action: runIssuesList,
|
|
||||||
Flags: append([]cli.Flag{
|
Flags: append([]cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.BoolFlag{
|
||||||
Name: "state",
|
Name: "comments",
|
||||||
Usage: "Filter by issue state (all|open|closed)",
|
Usage: "Whether to display comments (will prompt if not provided & run interactively)",
|
||||||
DefaultText: "open",
|
|
||||||
},
|
},
|
||||||
}, AllDefaultFlags...),
|
}, issues.CmdIssuesList.Flags...),
|
||||||
}
|
}
|
||||||
|
|
||||||
func runIssues(ctx *cli.Context) error {
|
func runIssues(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
if ctx.Args().Len() == 1 {
|
if cmd.Args().Len() == 1 {
|
||||||
return runIssueDetail(ctx, ctx.Args().First())
|
return runIssueDetail(ctx, cmd, cmd.Args().First())
|
||||||
}
|
}
|
||||||
return runIssuesList(ctx)
|
return issues.RunIssuesList(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runIssueDetail(ctx *cli.Context, index string) error {
|
func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
|
||||||
login, owner, repo := initCommand()
|
ctx, idx, err := resolveIssueDetailContext(cmd, index)
|
||||||
|
|
||||||
idx, err := argToIndex(index)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
issue, err := login.Client().GetIssue(owner, repo, idx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("#%d %s\n%s created %s\n\n%s\n", issue.Index,
|
return runIssueDetailWithClient(ctx, idx, ctx.Login.Client())
|
||||||
issue.Title,
|
|
||||||
issue.Poster.UserName,
|
|
||||||
issue.Created.Format("2006-01-02 15:04:05"),
|
|
||||||
issue.Body,
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runIssuesList(ctx *cli.Context) error {
|
func resolveIssueDetailContext(cmd *cli.Command, index string) (*context.TeaContext, int64, error) {
|
||||||
login, owner, repo := initCommand()
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
|
||||||
state := gitea.StateOpen
|
|
||||||
switch ctx.String("state") {
|
|
||||||
case "all":
|
|
||||||
state = gitea.StateAll
|
|
||||||
case "open":
|
|
||||||
state = gitea.StateOpen
|
|
||||||
case "closed":
|
|
||||||
state = gitea.StateClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
issues, err := login.Client().ListRepoIssues(owner, repo, gitea.ListIssueOption{
|
|
||||||
State: state,
|
|
||||||
Type: gitea.IssueTypeIssue,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if ctx.IsSet("owner") {
|
||||||
|
ctx.Owner = ctx.String("owner")
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
headers := []string{
|
idx, err := utils.ArgToIndex(index)
|
||||||
"Index",
|
if err != nil {
|
||||||
"State",
|
return nil, 0, err
|
||||||
"Author",
|
|
||||||
"Updated",
|
|
||||||
"Title",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var values [][]string
|
return ctx, idx, nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(issues) == 0 {
|
func runIssueDetailWithClient(ctx *context.TeaContext, idx int64, client issueDetailClient) error {
|
||||||
Output(outputValue, headers, values)
|
issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx)
|
||||||
return nil
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reactions, _, err := client.GetIssueReactions(ctx.Owner, ctx.Repo, idx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, issue := range issues {
|
if ctx.IsSet("output") {
|
||||||
name := issue.Poster.FullName
|
switch ctx.String("output") {
|
||||||
if len(name) == 0 {
|
case "json":
|
||||||
name = issue.Poster.UserName
|
return runIssueDetailAsJSON(ctx, issue)
|
||||||
}
|
}
|
||||||
values = append(
|
|
||||||
values,
|
|
||||||
[]string{
|
|
||||||
strconv.FormatInt(issue.Index, 10),
|
|
||||||
string(issue.State),
|
|
||||||
name,
|
|
||||||
issue.Updated.Format("2006-01-02 15:04:05"),
|
|
||||||
issue.Title,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Output(outputValue, headers, values)
|
|
||||||
|
|
||||||
return nil
|
print.IssueDetails(issue, reactions)
|
||||||
}
|
|
||||||
|
|
||||||
// CmdIssuesCreate represents a sub command of issues to create issue
|
if issue.Comments > 0 {
|
||||||
var CmdIssuesCreate = cli.Command{
|
err = interact.ShowCommentsMaybeInteractive(ctx, idx, issue.Comments)
|
||||||
Name: "create",
|
if err != nil {
|
||||||
Usage: "Create an issue on repository",
|
return fmt.Errorf("error loading comments: %v", err)
|
||||||
Description: `Create an issue on repository`,
|
}
|
||||||
Action: runIssuesCreate,
|
|
||||||
Flags: append([]cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "title",
|
|
||||||
Aliases: []string{"t"},
|
|
||||||
Usage: "issue title to create",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "body",
|
|
||||||
Aliases: []string{"b"},
|
|
||||||
Usage: "issue body to create",
|
|
||||||
},
|
|
||||||
}, LoginRepoFlags...),
|
|
||||||
}
|
|
||||||
|
|
||||||
func runIssuesCreate(ctx *cli.Context) error {
|
|
||||||
login, owner, repo := initCommand()
|
|
||||||
|
|
||||||
_, err := login.Client().CreateIssue(owner, repo, gitea.CreateIssueOption{
|
|
||||||
Title: ctx.String("title"),
|
|
||||||
Body: ctx.String("body"),
|
|
||||||
// TODO:
|
|
||||||
//Assignee string `json:"assignee"`
|
|
||||||
//Assignees []string `json:"assignees"`
|
|
||||||
//Deadline *time.Time `json:"due_date"`
|
|
||||||
//Milestone int64 `json:"milestone"`
|
|
||||||
//Labels []int64 `json:"labels"`
|
|
||||||
//Closed bool `json:"closed"`
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CmdIssuesReopen represents a sub command of issues to open an issue
|
func runIssueDetailAsJSON(ctx *context.TeaContext, issue *gitea.Issue) error {
|
||||||
var CmdIssuesReopen = cli.Command{
|
return runIssueDetailAsJSONWithClient(ctx, issue, ctx.Login.Client())
|
||||||
Name: "reopen",
|
|
||||||
Aliases: []string{"open"},
|
|
||||||
Usage: "Change state of an issue to 'open'",
|
|
||||||
Description: `Change state of an issue to 'open'`,
|
|
||||||
ArgsUsage: "<issue index>",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
var s = gitea.StateOpen
|
|
||||||
return editIssueState(ctx, gitea.EditIssueOption{State: &s})
|
|
||||||
},
|
|
||||||
Flags: AllDefaultFlags,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CmdIssuesClose represents a sub command of issues to close an issue
|
func runIssueDetailAsJSONWithClient(ctx *context.TeaContext, issue *gitea.Issue, c issueCommentClient) error {
|
||||||
var CmdIssuesClose = cli.Command{
|
opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions(ctx.Command)}
|
||||||
Name: "close",
|
comments := []*gitea.Comment{}
|
||||||
Usage: "Change state of an issue to 'closed'",
|
|
||||||
Description: `Change state of an issue to 'closed'`,
|
|
||||||
ArgsUsage: "<issue index>",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
var s = gitea.StateClosed
|
|
||||||
return editIssueState(ctx, gitea.EditIssueOption{State: &s})
|
|
||||||
},
|
|
||||||
Flags: AllDefaultFlags,
|
|
||||||
}
|
|
||||||
|
|
||||||
// editIssueState abstracts the arg parsing to edit the given issue
|
if ctx.Bool("comments") {
|
||||||
func editIssueState(ctx *cli.Context, opts gitea.EditIssueOption) error {
|
var err error
|
||||||
login, owner, repo := initCommand()
|
comments, _, err = c.ListIssueComments(ctx.Owner, ctx.Repo, issue.Index, opts)
|
||||||
if ctx.Args().Len() == 0 {
|
if err != nil {
|
||||||
log.Fatal(ctx.Command.ArgsUsage)
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index, err := argToIndex(ctx.Args().First())
|
return writeIndentedJSON(ctx.Writer, buildIssueData(issue, comments))
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
func buildIssueData(issue *gitea.Issue, comments []*gitea.Comment) issueData {
|
||||||
|
return issueData{
|
||||||
_, err = login.Client().EditIssue(owner, repo, index, opts)
|
ID: issue.ID,
|
||||||
return err
|
Index: issue.Index,
|
||||||
|
Title: issue.Title,
|
||||||
|
State: issue.State,
|
||||||
|
Created: issue.Created,
|
||||||
|
User: username(issue.Poster),
|
||||||
|
Body: issue.Body,
|
||||||
|
Labels: buildDetailLabels(issue.Labels),
|
||||||
|
Assignees: buildDetailAssignees(issue.Assignees),
|
||||||
|
URL: issue.HTMLURL,
|
||||||
|
ClosedAt: issue.Closed,
|
||||||
|
Comments: buildDetailComments(comments),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
cmd/issues/close.go
Normal file
64
cmd/issues/close.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdIssuesClose represents a sub command of issues to close an issue
|
||||||
|
var CmdIssuesClose = cli.Command{
|
||||||
|
Name: "close",
|
||||||
|
Usage: "Change state of one ore more issues to 'closed'",
|
||||||
|
Description: `Change state of one ore more issues to 'closed'`,
|
||||||
|
ArgsUsage: "<issue index> [<issue index>...]",
|
||||||
|
Action: func(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
s := gitea.StateClosed
|
||||||
|
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// editIssueState abstracts the arg parsing to edit the given issue
|
||||||
|
func editIssueState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOption) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ctx.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
for _, index := range indices {
|
||||||
|
issue, _, err := client.EditIssue(ctx.Owner, ctx.Repo, index, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(indices) > 1 {
|
||||||
|
fmt.Println(issue.HTMLURL)
|
||||||
|
} else {
|
||||||
|
print.IssueDetails(issue, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
cmd/issues/create.go
Normal file
56
cmd/issues/create.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdIssuesCreate represents a sub command of issues to create issue
|
||||||
|
var CmdIssuesCreate = cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Create an issue on repository",
|
||||||
|
Description: `Create an issue on repository`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: runIssuesCreate,
|
||||||
|
Flags: flags.IssuePRCreateFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsInteractiveMode() {
|
||||||
|
err := interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)
|
||||||
|
if err != nil && !interact.IsQuitting(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := flags.GetIssuePRCreateFlags(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.CreateIssue(
|
||||||
|
ctx.Login,
|
||||||
|
ctx.Owner,
|
||||||
|
ctx.Repo,
|
||||||
|
*opts,
|
||||||
|
)
|
||||||
|
}
|
||||||
80
cmd/issues/edit.go
Normal file
80
cmd/issues/edit.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdIssuesEdit is the subcommand of issues to edit issues
|
||||||
|
var CmdIssuesEdit = cli.Command{
|
||||||
|
Name: "edit",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Usage: "Edit one or more issues",
|
||||||
|
Description: `Edit one or more issues. To unset a property again,
|
||||||
|
use an empty string (eg. --milestone "").`,
|
||||||
|
ArgsUsage: "<idx> [<idx>...]",
|
||||||
|
Action: runIssuesEdit,
|
||||||
|
Flags: flags.IssuePREditFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmd.Args().Present() {
|
||||||
|
return fmt.Errorf("must specify at least one issue index")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := flags.GetIssuePREditFlags(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
for _, opts.Index = range indices {
|
||||||
|
if ctx.IsInteractiveMode() {
|
||||||
|
var err error
|
||||||
|
opts, err = interact.EditIssue(*ctx, opts.Index)
|
||||||
|
if err != nil {
|
||||||
|
if interact.IsQuitting(err) {
|
||||||
|
return nil // user quit
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := task.EditIssue(ctx, client, *opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ctx.Args().Len() > 1 {
|
||||||
|
fmt.Println(issue.HTMLURL)
|
||||||
|
} else {
|
||||||
|
print.IssueDetails(issue, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
116
cmd/issues/list.go
Normal file
116
cmd/issues/list.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var issueFieldsFlag = flags.FieldsFlag(print.IssueFields, []string{
|
||||||
|
"index", "title", "state", "author", "milestone", "labels", "owner", "repo",
|
||||||
|
})
|
||||||
|
|
||||||
|
// CmdIssuesList represents a sub command of issues to list issues
|
||||||
|
var CmdIssuesList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List issues of the repository",
|
||||||
|
Description: `List issues of the repository`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: RunIssuesList,
|
||||||
|
Flags: append([]cli.Flag{issueFieldsFlag}, flags.IssueListingFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunIssuesList list issues
|
||||||
|
func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := flags.ParseState(ctx.String("state"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeIssue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var from, until time.Time
|
||||||
|
if ctx.IsSet("from") {
|
||||||
|
from, err = dateparse.ParseLocal(ctx.String("from"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ctx.IsSet("until") {
|
||||||
|
until, err = dateparse.ParseLocal(ctx.String("until"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owner := ctx.Owner
|
||||||
|
if ctx.IsSet("owner") {
|
||||||
|
owner = ctx.String("owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore error, as we don't do any input validation on these flags
|
||||||
|
labels, _ := flags.LabelFilterFlag.GetValues(cmd)
|
||||||
|
milestones, _ := flags.MilestoneFilterFlag.GetValues(cmd)
|
||||||
|
var issues []*gitea.Issue
|
||||||
|
if ctx.Repo != "" {
|
||||||
|
issues, _, err = ctx.Login.Client().ListRepoIssues(owner, ctx.Repo, gitea.ListIssueOption{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
State: state,
|
||||||
|
Type: kind,
|
||||||
|
KeyWord: ctx.String("keyword"),
|
||||||
|
CreatedBy: ctx.String("author"),
|
||||||
|
AssignedBy: ctx.String("assigned-to"),
|
||||||
|
MentionedBy: ctx.String("mentions"),
|
||||||
|
Labels: labels,
|
||||||
|
Milestones: milestones,
|
||||||
|
Since: from,
|
||||||
|
Before: until,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
issues, _, err = ctx.Login.Client().ListIssues(gitea.ListIssueOption{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
State: state,
|
||||||
|
Type: kind,
|
||||||
|
KeyWord: ctx.String("keyword"),
|
||||||
|
CreatedBy: ctx.String("author"),
|
||||||
|
AssignedBy: ctx.String("assigned-to"),
|
||||||
|
MentionedBy: ctx.String("mentions"),
|
||||||
|
Labels: labels,
|
||||||
|
Milestones: milestones,
|
||||||
|
Since: from,
|
||||||
|
Before: until,
|
||||||
|
Owner: owner,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fields, err := issueFieldsFlag.GetValues(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.IssuesPullsList(issues, ctx.Output, fields)
|
||||||
|
}
|
||||||
27
cmd/issues/reopen.go
Normal file
27
cmd/issues/reopen.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdIssuesReopen represents a sub command of issues to open an issue
|
||||||
|
var CmdIssuesReopen = cli.Command{
|
||||||
|
Name: "reopen",
|
||||||
|
Aliases: []string{"open"},
|
||||||
|
Usage: "Change state of one or more issues to 'open'",
|
||||||
|
Description: `Change state of one or more issues to 'open'`,
|
||||||
|
ArgsUsage: "<issue index> [<issue index>...]",
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
s := gitea.StateOpen
|
||||||
|
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
358
cmd/issues_test.go
Normal file
358
cmd/issues_test.go
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testOwner = "testOwner"
|
||||||
|
testRepo = "testRepo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeIssueCommentClient struct {
|
||||||
|
owner string
|
||||||
|
repo string
|
||||||
|
index int64
|
||||||
|
comments []*gitea.Comment
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeIssueCommentClient) ListIssueComments(owner, repo string, index int64, _ gitea.ListIssueCommentOptions) ([]*gitea.Comment, *gitea.Response, error) {
|
||||||
|
f.owner = owner
|
||||||
|
f.repo = repo
|
||||||
|
f.index = index
|
||||||
|
return f.comments, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeIssueDetailClient struct {
|
||||||
|
owner string
|
||||||
|
repo string
|
||||||
|
index int64
|
||||||
|
issue *gitea.Issue
|
||||||
|
reactions []*gitea.Reaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeIssueDetailClient) GetIssue(owner, repo string, index int64) (*gitea.Issue, *gitea.Response, error) {
|
||||||
|
f.owner = owner
|
||||||
|
f.repo = repo
|
||||||
|
f.index = index
|
||||||
|
return f.issue, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeIssueDetailClient) GetIssueReactions(owner, repo string, index int64) ([]*gitea.Reaction, *gitea.Response, error) {
|
||||||
|
f.owner = owner
|
||||||
|
f.repo = repo
|
||||||
|
f.index = index
|
||||||
|
return f.reactions, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCommentPointers(comments []gitea.Comment) []*gitea.Comment {
|
||||||
|
result := make([]*gitea.Comment, 0, len(comments))
|
||||||
|
for i := range comments {
|
||||||
|
comment := comments[i]
|
||||||
|
result = append(result, &comment)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestIssue(comments int, isClosed bool) gitea.Issue {
|
||||||
|
issue := gitea.Issue{
|
||||||
|
ID: 42,
|
||||||
|
Index: 1,
|
||||||
|
Title: "Test issue",
|
||||||
|
State: gitea.StateOpen,
|
||||||
|
Body: "This is a test",
|
||||||
|
Created: time.Date(2025, 31, 10, 23, 59, 59, 999999999, time.UTC),
|
||||||
|
Updated: time.Date(2025, 1, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Labels: []*gitea.Label{
|
||||||
|
{
|
||||||
|
Name: "example/Label1",
|
||||||
|
Color: "very red",
|
||||||
|
Description: "This is an example label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "example/Label2",
|
||||||
|
Color: "hardly red",
|
||||||
|
Description: "This is another example label",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Comments: comments,
|
||||||
|
Poster: &gitea.User{
|
||||||
|
UserName: "testUser",
|
||||||
|
},
|
||||||
|
Assignees: []*gitea.User{
|
||||||
|
{UserName: "testUser"},
|
||||||
|
{UserName: "testUser3"},
|
||||||
|
},
|
||||||
|
HTMLURL: "<space holder>",
|
||||||
|
Closed: nil, // 2025-11-10T21:20:19Z
|
||||||
|
}
|
||||||
|
|
||||||
|
if isClosed {
|
||||||
|
closed := time.Date(2025, 11, 10, 21, 20, 19, 0, time.UTC)
|
||||||
|
issue.Closed = &closed
|
||||||
|
}
|
||||||
|
|
||||||
|
if isClosed {
|
||||||
|
issue.State = gitea.StateClosed
|
||||||
|
} else {
|
||||||
|
issue.State = gitea.StateOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestIssueComments(comments int) []gitea.Comment {
|
||||||
|
baseID := 900
|
||||||
|
var result []gitea.Comment
|
||||||
|
|
||||||
|
for commentID := 0; commentID < comments; commentID++ {
|
||||||
|
result = append(result, gitea.Comment{
|
||||||
|
ID: int64(baseID + commentID),
|
||||||
|
Poster: &gitea.User{
|
||||||
|
UserName: "Freddy",
|
||||||
|
},
|
||||||
|
Body: fmt.Sprintf("This is a test comment #%v", commentID),
|
||||||
|
Created: time.Date(2025, 11, 3, 12, 0, 0, 0, time.UTC).
|
||||||
|
Add(time.Duration(commentID) * time.Hour),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunIssueDetailAsJSON(t *testing.T) {
|
||||||
|
type TestCase struct {
|
||||||
|
name string
|
||||||
|
issue gitea.Issue
|
||||||
|
comments []gitea.Comment
|
||||||
|
flagComments bool
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := cli.Command{
|
||||||
|
Name: "t",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "comments",
|
||||||
|
Value: false,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Value: "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext := context.TeaContext{
|
||||||
|
Owner: testOwner,
|
||||||
|
Repo: testRepo,
|
||||||
|
Login: &config.Login{
|
||||||
|
Name: "testLogin",
|
||||||
|
URL: "http://127.0.0.1:8081",
|
||||||
|
},
|
||||||
|
Command: &cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []TestCase{
|
||||||
|
{
|
||||||
|
name: "Simple issue with no comments, no comments requested",
|
||||||
|
issue: createTestIssue(0, true),
|
||||||
|
comments: []gitea.Comment{},
|
||||||
|
flagComments: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Simple issue with no comments, comments requested",
|
||||||
|
issue: createTestIssue(0, true),
|
||||||
|
comments: []gitea.Comment{},
|
||||||
|
flagComments: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Simple issue with comments, no comments requested",
|
||||||
|
issue: createTestIssue(2, true),
|
||||||
|
comments: createTestIssueComments(2),
|
||||||
|
flagComments: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Simple issue with comments, comments requested",
|
||||||
|
issue: createTestIssue(2, true),
|
||||||
|
comments: createTestIssueComments(2),
|
||||||
|
flagComments: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Simple issue with comments, comments requested, not closed",
|
||||||
|
issue: createTestIssue(2, false),
|
||||||
|
comments: createTestIssueComments(2),
|
||||||
|
flagComments: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
client := &fakeIssueCommentClient{
|
||||||
|
comments: toCommentPointers(testCase.comments),
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext.Login.URL = "https://gitea.example.com"
|
||||||
|
testCase.issue.HTMLURL = fmt.Sprintf("%s/%s/%s/issues/%d/", testContext.Login.URL, testOwner, testRepo, testCase.issue.Index)
|
||||||
|
|
||||||
|
var outBuffer bytes.Buffer
|
||||||
|
testContext.Writer = &outBuffer
|
||||||
|
var errBuffer bytes.Buffer
|
||||||
|
testContext.ErrWriter = &errBuffer
|
||||||
|
|
||||||
|
if testCase.flagComments {
|
||||||
|
require.NoError(t, testContext.Set("comments", "true"))
|
||||||
|
} else {
|
||||||
|
require.NoError(t, testContext.Set("comments", "false"))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := runIssueDetailAsJSONWithClient(&testContext, &testCase.issue, client)
|
||||||
|
|
||||||
|
require.NoError(t, err, "Failed to run issue detail as JSON")
|
||||||
|
if testCase.flagComments {
|
||||||
|
assert.Equal(t, testOwner, client.owner)
|
||||||
|
assert.Equal(t, testRepo, client.repo)
|
||||||
|
assert.Equal(t, testCase.issue.Index, client.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := outBuffer.String()
|
||||||
|
|
||||||
|
require.NotEmpty(t, out, "Unexpected empty output from runIssueDetailAsJSON")
|
||||||
|
|
||||||
|
// setting expectations
|
||||||
|
|
||||||
|
var expectedLabels []labelData
|
||||||
|
expectedLabels = []labelData{}
|
||||||
|
for _, l := range testCase.issue.Labels {
|
||||||
|
expectedLabels = append(expectedLabels, labelData{
|
||||||
|
Name: l.Name,
|
||||||
|
Color: l.Color,
|
||||||
|
Description: l.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedAssignees []string
|
||||||
|
expectedAssignees = []string{}
|
||||||
|
for _, a := range testCase.issue.Assignees {
|
||||||
|
expectedAssignees = append(expectedAssignees, a.UserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedClosedAt *time.Time
|
||||||
|
if testCase.issue.Closed != nil {
|
||||||
|
expectedClosedAt = testCase.issue.Closed
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedComments []commentData
|
||||||
|
expectedComments = []commentData{}
|
||||||
|
if testCase.flagComments {
|
||||||
|
for _, c := range testCase.comments {
|
||||||
|
expectedComments = append(expectedComments, commentData{
|
||||||
|
ID: c.ID,
|
||||||
|
Author: c.Poster.UserName,
|
||||||
|
Body: c.Body,
|
||||||
|
Created: c.Created,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := issueData{
|
||||||
|
ID: testCase.issue.ID,
|
||||||
|
Index: testCase.issue.Index,
|
||||||
|
Title: testCase.issue.Title,
|
||||||
|
State: testCase.issue.State,
|
||||||
|
Created: testCase.issue.Created,
|
||||||
|
User: testCase.issue.Poster.UserName,
|
||||||
|
Body: testCase.issue.Body,
|
||||||
|
URL: testCase.issue.HTMLURL,
|
||||||
|
ClosedAt: expectedClosedAt,
|
||||||
|
Labels: expectedLabels,
|
||||||
|
Assignees: expectedAssignees,
|
||||||
|
Comments: expectedComments,
|
||||||
|
}
|
||||||
|
|
||||||
|
// validating reality
|
||||||
|
var actual issueData
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(outBuffer.Bytes()))
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
err = dec.Decode(&actual)
|
||||||
|
require.NoError(t, err, "Failed to unmarshal output into struct")
|
||||||
|
|
||||||
|
assert.Equal(t, expected, actual, "Expected structs differ from expected one")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunIssueDetailUsesOwnerFlag(t *testing.T) {
|
||||||
|
issueIndex := int64(12)
|
||||||
|
expectedOwner := "overrideOwner"
|
||||||
|
expectedRepo := "overrideRepo"
|
||||||
|
issue := &gitea.Issue{
|
||||||
|
ID: 99,
|
||||||
|
Index: issueIndex,
|
||||||
|
Title: "Owner override test",
|
||||||
|
State: gitea.StateOpen,
|
||||||
|
Created: time.Date(2025, 11, 1, 10, 0, 0, 0, time.UTC),
|
||||||
|
Poster: &gitea.User{
|
||||||
|
UserName: "tester",
|
||||||
|
},
|
||||||
|
HTMLURL: "https://example.test/issues/12",
|
||||||
|
}
|
||||||
|
|
||||||
|
config.SetConfigForTesting(config.LocalConfig{
|
||||||
|
Logins: []config.Login{{
|
||||||
|
Name: "testLogin",
|
||||||
|
URL: "https://gitea.example.com",
|
||||||
|
Token: "token",
|
||||||
|
User: "loginUser",
|
||||||
|
Default: true,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := cli.Command{
|
||||||
|
Name: "issues",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&flags.LoginFlag,
|
||||||
|
&flags.RepoFlag,
|
||||||
|
&flags.RemoteFlag,
|
||||||
|
&flags.OutputFlag,
|
||||||
|
&cli.StringFlag{Name: "owner"},
|
||||||
|
&cli.BoolFlag{Name: "comments"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var outBuffer bytes.Buffer
|
||||||
|
var errBuffer bytes.Buffer
|
||||||
|
cmd.Writer = &outBuffer
|
||||||
|
cmd.ErrWriter = &errBuffer
|
||||||
|
require.NoError(t, cmd.Set("login", "testLogin"))
|
||||||
|
require.NoError(t, cmd.Set("repo", expectedRepo))
|
||||||
|
require.NoError(t, cmd.Set("owner", expectedOwner))
|
||||||
|
require.NoError(t, cmd.Set("comments", "false"))
|
||||||
|
|
||||||
|
teaCtx, idx, err := resolveIssueDetailContext(&cmd, fmt.Sprintf("%d", issueIndex))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client := &fakeIssueDetailClient{
|
||||||
|
issue: issue,
|
||||||
|
reactions: []*gitea.Reaction{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runIssueDetailWithClient(teaCtx, idx, client)
|
||||||
|
require.NoError(t, err, "Expected runIssueDetail to succeed")
|
||||||
|
assert.Equal(t, expectedOwner, client.owner)
|
||||||
|
assert.Equal(t, expectedRepo, client.repo)
|
||||||
|
assert.Equal(t, issueIndex, client.index)
|
||||||
|
}
|
||||||
263
cmd/labels.go
263
cmd/labels.go
@@ -1,266 +1,41 @@
|
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// SPDX-License-Identifier: MIT
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/tea/cmd/labels"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CmdLabels represents to operate repositories' labels.
|
// CmdLabels represents to operate repositories' labels.
|
||||||
var CmdLabels = cli.Command{
|
var CmdLabels = cli.Command{
|
||||||
Name: "labels",
|
Name: "labels",
|
||||||
|
Aliases: []string{"label"},
|
||||||
|
Category: catEntities,
|
||||||
Usage: "Manage issue labels",
|
Usage: "Manage issue labels",
|
||||||
Description: `Manage issue labels`,
|
Description: `Manage issue labels`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
Action: runLabels,
|
Action: runLabels,
|
||||||
Subcommands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
&CmdLabelCreate,
|
&labels.CmdLabelsList,
|
||||||
&CmdLabelUpdate,
|
&labels.CmdLabelCreate,
|
||||||
&CmdLabelDelete,
|
&labels.CmdLabelUpdate,
|
||||||
|
&labels.CmdLabelDelete,
|
||||||
},
|
},
|
||||||
Flags: append([]cli.Flag{
|
Flags: labels.CmdLabelsList.Flags,
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "save",
|
|
||||||
Aliases: []string{"s"},
|
|
||||||
Usage: "Save all the labels as a file",
|
|
||||||
},
|
|
||||||
}, AllDefaultFlags...),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runLabels(ctx *cli.Context) error {
|
func runLabels(ctx context.Context, cmd *cli.Command) error {
|
||||||
login, owner, repo := initCommand()
|
if cmd.Args().Len() == 1 {
|
||||||
|
return runLabelsDetails(cmd)
|
||||||
headers := []string{
|
|
||||||
"Index",
|
|
||||||
"Color",
|
|
||||||
"Name",
|
|
||||||
"Description",
|
|
||||||
}
|
}
|
||||||
|
return labels.RunLabelsList(ctx, cmd)
|
||||||
var values [][]string
|
|
||||||
|
|
||||||
labels, err := login.Client().ListRepoLabels(owner, repo, gitea.ListLabelsOptions{})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(labels) == 0 {
|
|
||||||
Output(outputValue, headers, values)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fPath := ctx.String("save")
|
|
||||||
if len(fPath) > 0 {
|
|
||||||
f, err := os.Create(fPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
for _, label := range labels {
|
|
||||||
fmt.Fprintf(f, "#%s %s\n", label.Color, label.Name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, label := range labels {
|
|
||||||
values = append(
|
|
||||||
values,
|
|
||||||
[]string{
|
|
||||||
strconv.FormatInt(label.ID, 10),
|
|
||||||
label.Color,
|
|
||||||
label.Name,
|
|
||||||
label.Description,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Output(outputValue, headers, values)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CmdLabelCreate represents a sub command of labels to create label.
|
func runLabelsDetails(cmd *cli.Command) error {
|
||||||
var CmdLabelCreate = cli.Command{
|
return fmt.Errorf("not yet implemented")
|
||||||
Name: "create",
|
|
||||||
Usage: "Create a label",
|
|
||||||
Description: `Create a label`,
|
|
||||||
Action: runLabelCreate,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "name",
|
|
||||||
Usage: "label name",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "color",
|
|
||||||
Usage: "label color value",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "description",
|
|
||||||
Usage: "label description",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "file",
|
|
||||||
Usage: "indicate a label file",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitLabelLine(line string) (string, string, string) {
|
|
||||||
fields := strings.SplitN(line, ";", 2)
|
|
||||||
var color, name, description string
|
|
||||||
if len(fields) < 1 {
|
|
||||||
return "", "", ""
|
|
||||||
} else if len(fields) >= 2 {
|
|
||||||
description = strings.TrimSpace(fields[1])
|
|
||||||
}
|
|
||||||
fields = strings.Fields(fields[0])
|
|
||||||
if len(fields) <= 0 {
|
|
||||||
return "", "", ""
|
|
||||||
}
|
|
||||||
color = fields[0]
|
|
||||||
if len(fields) == 2 {
|
|
||||||
name = fields[1]
|
|
||||||
} else if len(fields) > 2 {
|
|
||||||
name = strings.Join(fields[1:], " ")
|
|
||||||
}
|
|
||||||
return color, name, description
|
|
||||||
}
|
|
||||||
|
|
||||||
func runLabelCreate(ctx *cli.Context) error {
|
|
||||||
login, owner, repo := initCommand()
|
|
||||||
|
|
||||||
labelFile := ctx.String("file")
|
|
||||||
var err error
|
|
||||||
if len(labelFile) == 0 {
|
|
||||||
_, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{
|
|
||||||
Name: ctx.String("name"),
|
|
||||||
Color: ctx.String("color"),
|
|
||||||
Description: ctx.String("description"),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
f, err := os.Open(labelFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(f)
|
|
||||||
var i = 1
|
|
||||||
// FIXME: if Gitea's API support create multiple labels once, we should move to that API.
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
color, name, description := splitLabelLine(line)
|
|
||||||
if color == "" || name == "" {
|
|
||||||
log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line)
|
|
||||||
} else {
|
|
||||||
_, err = login.Client().CreateLabel(owner, repo, gitea.CreateLabelOption{
|
|
||||||
Name: name,
|
|
||||||
Color: color,
|
|
||||||
Description: description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdLabelUpdate represents a sub command of labels to update label.
|
|
||||||
var CmdLabelUpdate = cli.Command{
|
|
||||||
Name: "update",
|
|
||||||
Usage: "Update a label",
|
|
||||||
Description: `Update a label`,
|
|
||||||
Action: runLabelUpdate,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "id",
|
|
||||||
Usage: "label id",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "name",
|
|
||||||
Usage: "label name",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "color",
|
|
||||||
Usage: "label color value",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "description",
|
|
||||||
Usage: "label description",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runLabelUpdate(ctx *cli.Context) error {
|
|
||||||
login, owner, repo := initCommand()
|
|
||||||
|
|
||||||
id := ctx.Int64("id")
|
|
||||||
var pName, pColor, pDescription *string
|
|
||||||
name := ctx.String("name")
|
|
||||||
if name != "" {
|
|
||||||
pName = &name
|
|
||||||
}
|
|
||||||
|
|
||||||
color := ctx.String("color")
|
|
||||||
if color != "" {
|
|
||||||
pColor = &color
|
|
||||||
}
|
|
||||||
|
|
||||||
description := ctx.String("description")
|
|
||||||
if description != "" {
|
|
||||||
pDescription = &description
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
_, err = login.Client().EditLabel(owner, repo, id, gitea.EditLabelOption{
|
|
||||||
Name: pName,
|
|
||||||
Color: pColor,
|
|
||||||
Description: pDescription,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdLabelDelete represents a sub command of labels to delete label.
|
|
||||||
var CmdLabelDelete = cli.Command{
|
|
||||||
Name: "delete",
|
|
||||||
Usage: "Delete a label",
|
|
||||||
Description: `Delete a label`,
|
|
||||||
Action: runLabelCreate,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "id",
|
|
||||||
Usage: "label id",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runLabelDelete(ctx *cli.Context) error {
|
|
||||||
login, owner, repo := initCommand()
|
|
||||||
|
|
||||||
err := login.Client().DeleteLabel(owner, repo, ctx.Int64("id"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
116
cmd/labels/create.go
Normal file
116
cmd/labels/create.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package labels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
stdctx "context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLabelCreate represents a sub command of labels to create label.
|
||||||
|
var CmdLabelCreate = cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Create a label",
|
||||||
|
Description: `Create a label`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: runLabelCreate,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Usage: "label name",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "color",
|
||||||
|
Usage: "label color value",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Usage: "label description",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "file",
|
||||||
|
Usage: "indicate a label file",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
labelFile := ctx.String("file")
|
||||||
|
if len(labelFile) == 0 {
|
||||||
|
_, _, err := ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
|
||||||
|
Name: ctx.String("name"),
|
||||||
|
Color: ctx.String("color"),
|
||||||
|
Description: ctx.String("description"),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(labelFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
i := 1
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
color, name, description := splitLabelLine(line)
|
||||||
|
if color == "" || name == "" {
|
||||||
|
log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line)
|
||||||
|
} else {
|
||||||
|
_, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
|
||||||
|
Name: name,
|
||||||
|
Color: color,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitLabelLine(line string) (string, string, string) {
|
||||||
|
fields := strings.SplitN(line, ";", 2)
|
||||||
|
var color, name, description string
|
||||||
|
if len(fields) < 1 {
|
||||||
|
return "", "", ""
|
||||||
|
} else if len(fields) >= 2 {
|
||||||
|
description = strings.TrimSpace(fields[1])
|
||||||
|
}
|
||||||
|
fields = strings.Fields(fields[0])
|
||||||
|
if len(fields) <= 0 {
|
||||||
|
return "", "", ""
|
||||||
|
}
|
||||||
|
color = fields[0]
|
||||||
|
if len(fields) == 2 {
|
||||||
|
name = fields[1]
|
||||||
|
} else if len(fields) > 2 {
|
||||||
|
name = strings.Join(fields[1:], " ")
|
||||||
|
}
|
||||||
|
return color, name, description
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// SPDX-License-Identifier: MIT
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
package labels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -21,7 +20,7 @@ func TestParseLabelLine(t *testing.T) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
scanner := bufio.NewScanner(strings.NewReader(labels))
|
scanner := bufio.NewScanner(strings.NewReader(labels))
|
||||||
var i = 1
|
i := 1
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
color, name, description := splitLabelLine(line)
|
color, name, description := splitLabelLine(line)
|
||||||
58
cmd/labels/delete.go
Normal file
58
cmd/labels/delete.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package labels
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLabelDelete represents a sub command of labels to delete label.
|
||||||
|
var CmdLabelDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "Delete a label",
|
||||||
|
Description: `Delete a label`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: runLabelDelete,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "id",
|
||||||
|
Usage: "label id",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLabelDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
labelID := ctx.Int64("id")
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
|
||||||
|
// Verify the label exists first
|
||||||
|
label, _, err := client.GetRepoLabel(ctx.Owner, ctx.Repo, labelID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get label %d: %w", labelID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DeleteLabel(ctx.Owner, ctx.Repo, labelID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete label '%s' (id: %d): %w", label.Name, labelID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Label '%s' (id: %d) deleted successfully\n", label.Name, labelID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
60
cmd/labels/list.go
Normal file
60
cmd/labels/list.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package labels
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLabelsList represents a sub command of labels to list labels
|
||||||
|
var CmdLabelsList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List labels",
|
||||||
|
Description: "List labels",
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: RunLabelsList,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "save",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "Save all the labels as a file",
|
||||||
|
},
|
||||||
|
&flags.PaginationPageFlag,
|
||||||
|
&flags.PaginationLimitFlag,
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunLabelsList list labels.
|
||||||
|
func RunLabelsList(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
labels, _, err := client.ListRepoLabels(ctx.Owner, ctx.Repo, gitea.ListLabelsOptions{
|
||||||
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsSet("save") {
|
||||||
|
return task.LabelsExport(labels, ctx.String("save"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return print.LabelsList(labels, ctx.Output)
|
||||||
|
}
|
||||||
79
cmd/labels/update.go
Normal file
79
cmd/labels/update.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package labels
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLabelUpdate represents a sub command of labels to update label.
|
||||||
|
var CmdLabelUpdate = cli.Command{
|
||||||
|
Name: "update",
|
||||||
|
Usage: "Update a label",
|
||||||
|
Description: `Update a label`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: runLabelUpdate,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "id",
|
||||||
|
Usage: "label id",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Usage: "label name",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "color",
|
||||||
|
Usage: "label color value",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Usage: "label description",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
id := ctx.Int64("id")
|
||||||
|
var pName, pColor, pDescription *string
|
||||||
|
name := ctx.String("name")
|
||||||
|
if name != "" {
|
||||||
|
pName = &name
|
||||||
|
}
|
||||||
|
|
||||||
|
color := ctx.String("color")
|
||||||
|
if color != "" {
|
||||||
|
pColor = &color
|
||||||
|
}
|
||||||
|
|
||||||
|
description := ctx.String("description")
|
||||||
|
if description != "" {
|
||||||
|
pDescription = &description
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = ctx.Login.Client().EditLabel(ctx.Owner, ctx.Repo, id, gitea.EditLabelOption{
|
||||||
|
Name: pName,
|
||||||
|
Color: pColor,
|
||||||
|
Description: pDescription,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
111
cmd/log.go
111
cmd/log.go
@@ -1,111 +0,0 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
showLog bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// Println println content according the flag
|
|
||||||
func Println(a ...interface{}) {
|
|
||||||
if showLog {
|
|
||||||
fmt.Println(a...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf printf content according the flag
|
|
||||||
func Printf(format string, a ...interface{}) {
|
|
||||||
if showLog {
|
|
||||||
fmt.Printf(format, a...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error println content as an error information
|
|
||||||
func Error(a ...interface{}) {
|
|
||||||
fmt.Println(a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errorf printf content as an error information
|
|
||||||
func Errorf(format string, a ...interface{}) {
|
|
||||||
fmt.Printf(format, a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputtable prints structured data as table
|
|
||||||
func outputtable(headers []string, values [][]string) {
|
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
|
||||||
if len(headers) > 0 {
|
|
||||||
table.SetHeader(headers)
|
|
||||||
}
|
|
||||||
for _, value := range values {
|
|
||||||
table.Append(value)
|
|
||||||
}
|
|
||||||
table.Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputsimple prints structured data as space delimited value
|
|
||||||
func outputsimple(headers []string, values [][]string) {
|
|
||||||
for _, value := range values {
|
|
||||||
fmt.Printf(strings.Join(value, " "))
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputdsv prints structured data as delimiter separated value format
|
|
||||||
func outputdsv(headers []string, values [][]string, delimiterOpt ...string) {
|
|
||||||
delimiter := ","
|
|
||||||
if len(delimiterOpt) > 0 {
|
|
||||||
delimiter = delimiterOpt[0]
|
|
||||||
}
|
|
||||||
fmt.Println("\"" + strings.Join(headers, "\""+delimiter+"\"") + "\"")
|
|
||||||
for _, value := range values {
|
|
||||||
fmt.Printf("\"")
|
|
||||||
fmt.Printf(strings.Join(value, "\""+delimiter+"\""))
|
|
||||||
fmt.Printf("\"")
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputyaml prints structured data as yaml
|
|
||||||
func outputyaml(headers []string, values [][]string) {
|
|
||||||
for _, value := range values {
|
|
||||||
fmt.Println("-")
|
|
||||||
for j, val := range value {
|
|
||||||
intVal, _ := strconv.Atoi(val)
|
|
||||||
if strconv.Itoa(intVal) == val {
|
|
||||||
fmt.Printf(" %s: %s\n", headers[j], val)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" %s: '%s'\n", headers[j], val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output provides general function to convert given information
|
|
||||||
// into several outputs
|
|
||||||
func Output(output string, headers []string, values [][]string) {
|
|
||||||
switch {
|
|
||||||
case output == "" || output == "table":
|
|
||||||
outputtable(headers, values)
|
|
||||||
case output == "csv":
|
|
||||||
outputdsv(headers, values, ",")
|
|
||||||
case output == "simple":
|
|
||||||
outputsimple(headers, values)
|
|
||||||
case output == "tsv":
|
|
||||||
outputdsv(headers, values, "\t")
|
|
||||||
case output == "yaml":
|
|
||||||
outputyaml(headers, values)
|
|
||||||
default:
|
|
||||||
Errorf("unknown output type '" + output + "', available types are:\n- csv: comma-separated values\n- simple: space-separated values\n- table: auto-aligned table format (default)\n- tsv: tab-separated values\n- yaml: YAML format\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
171
cmd/login.go
171
cmd/login.go
@@ -1,161 +1,56 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// SPDX-License-Identifier: MIT
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/tea/cmd/login"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CmdLogin represents to login a gitea server.
|
// CmdLogin represents to login a gitea server.
|
||||||
var CmdLogin = cli.Command{
|
var CmdLogin = cli.Command{
|
||||||
Name: "login",
|
Name: "logins",
|
||||||
|
Aliases: []string{"login"},
|
||||||
|
Category: catSetup,
|
||||||
Usage: "Log in to a Gitea server",
|
Usage: "Log in to a Gitea server",
|
||||||
Description: `Log in to a Gitea server`,
|
Description: `Log in to a Gitea server`,
|
||||||
Action: runLoginList,
|
ArgsUsage: "[<login name>]",
|
||||||
Subcommands: []*cli.Command{
|
Action: runLogins,
|
||||||
&cmdLoginList,
|
Commands: []*cli.Command{
|
||||||
&cmdLoginAdd,
|
&login.CmdLoginList,
|
||||||
|
&login.CmdLoginAdd,
|
||||||
|
&login.CmdLoginEdit,
|
||||||
|
&login.CmdLoginDelete,
|
||||||
|
&login.CmdLoginSetDefault,
|
||||||
|
&login.CmdLoginHelper,
|
||||||
|
&login.CmdLoginOAuthRefresh,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// CmdLogin represents to login a gitea server.
|
func runLogins(ctx context.Context, cmd *cli.Command) error {
|
||||||
var cmdLoginAdd = cli.Command{
|
if cmd.Args().Len() == 1 {
|
||||||
Name: "add",
|
return runLoginDetail(cmd.Args().First())
|
||||||
Usage: "Add a Gitea login",
|
}
|
||||||
Description: `Add a Gitea login`,
|
return login.RunLoginList(ctx, cmd)
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "name",
|
|
||||||
Aliases: []string{"n"},
|
|
||||||
Usage: "Login name",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "url",
|
|
||||||
Aliases: []string{"u"},
|
|
||||||
Value: "https://try.gitea.io",
|
|
||||||
EnvVars: []string{"GITEA_SERVER_URL"},
|
|
||||||
Usage: "Server URL",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "token",
|
|
||||||
Aliases: []string{"t"},
|
|
||||||
Value: "",
|
|
||||||
EnvVars: []string{"GITEA_SERVER_TOKEN"},
|
|
||||||
Usage: "Access token. Can be obtained from Settings > Applications",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "ssh-key",
|
|
||||||
Aliases: []string{"s"},
|
|
||||||
Usage: "Path to a SSH key to use for pull/push operations",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "insecure",
|
|
||||||
Aliases: []string{"i"},
|
|
||||||
Usage: "Disable TLS verification",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Action: runLoginAdd,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runLoginAdd(ctx *cli.Context) error {
|
func runLoginDetail(name string) error {
|
||||||
if !ctx.IsSet("url") {
|
l, err := config.GetLoginByName(name)
|
||||||
log.Fatal("You have to input Gitea server URL")
|
|
||||||
}
|
|
||||||
if !ctx.IsSet("token") {
|
|
||||||
log.Fatal("No token found")
|
|
||||||
}
|
|
||||||
if !ctx.IsSet("name") {
|
|
||||||
log.Fatal("You have to set a name for the login")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := loadConfig(yamlConfigPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Unable to load config file " + yamlConfigPath)
|
return err
|
||||||
}
|
}
|
||||||
|
if l == nil {
|
||||||
client := gitea.NewClient(ctx.String("url"), ctx.String("token"))
|
fmt.Printf("Login '%s' do not exist\n\n", name)
|
||||||
if ctx.Bool("insecure") {
|
return nil
|
||||||
cookieJar, _ := cookiejar.New(nil)
|
|
||||||
|
|
||||||
client.SetHTTPClient(&http.Client{
|
|
||||||
Jar: cookieJar,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
u, err := client.GetMyUserInfo()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Login successful! Login name " + u.UserName)
|
|
||||||
|
|
||||||
err = addLogin(Login{
|
|
||||||
Name: ctx.String("name"),
|
|
||||||
URL: ctx.String("url"),
|
|
||||||
Token: ctx.String("token"),
|
|
||||||
Insecure: ctx.Bool("insecure"),
|
|
||||||
SSHKey: ctx.String("ssh-key"),
|
|
||||||
User: u.UserName,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = saveConfig(yamlConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
print.LoginDetails(l)
|
||||||
}
|
|
||||||
|
|
||||||
// CmdLogin represents to login a gitea server.
|
|
||||||
var cmdLoginList = cli.Command{
|
|
||||||
Name: "ls",
|
|
||||||
Usage: "List Gitea logins",
|
|
||||||
Description: `List Gitea logins`,
|
|
||||||
Action: runLoginList,
|
|
||||||
Flags: []cli.Flag{&OutputFlag},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runLoginList(ctx *cli.Context) error {
|
|
||||||
err := loadConfig(yamlConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Unable to load config file " + yamlConfigPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := []string{
|
|
||||||
"Name",
|
|
||||||
"URL",
|
|
||||||
"SSHHost",
|
|
||||||
}
|
|
||||||
|
|
||||||
var values [][]string
|
|
||||||
|
|
||||||
for _, l := range config.Logins {
|
|
||||||
values = append(values, []string{
|
|
||||||
l.Name,
|
|
||||||
l.URL,
|
|
||||||
l.GetSSHHost(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Output(outputValue, headers, values)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
165
cmd/login/add.go
Normal file
165
cmd/login/add.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/auth"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLoginAdd represents to login a gitea server.
|
||||||
|
var CmdLoginAdd = cli.Command{
|
||||||
|
Name: "add",
|
||||||
|
Usage: "Add a Gitea login",
|
||||||
|
Description: `Add a Gitea login, without args it will create one interactively`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
Usage: "Login name",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "url",
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
Value: "https://gitea.com",
|
||||||
|
Sources: cli.EnvVars("GITEA_SERVER_URL"),
|
||||||
|
Usage: "Server URL",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "no-version-check",
|
||||||
|
Aliases: []string{"nv"},
|
||||||
|
Usage: "Do not check version of Gitea instance",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "token",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Value: "",
|
||||||
|
Sources: cli.EnvVars("GITEA_SERVER_TOKEN"),
|
||||||
|
Usage: "Access token. Can be obtained from Settings > Applications",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "user",
|
||||||
|
Value: "",
|
||||||
|
Sources: cli.EnvVars("GITEA_SERVER_USER"),
|
||||||
|
Usage: "User for basic auth (will create token)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "password",
|
||||||
|
Aliases: []string{"pwd"},
|
||||||
|
Value: "",
|
||||||
|
Sources: cli.EnvVars("GITEA_SERVER_PASSWORD"),
|
||||||
|
Usage: "Password for basic auth (will create token)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "otp",
|
||||||
|
Sources: cli.EnvVars("GITEA_SERVER_OTP"),
|
||||||
|
Usage: "OTP token for auth, if necessary",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "scopes",
|
||||||
|
Sources: cli.EnvVars("GITEA_SCOPES"),
|
||||||
|
Usage: "Token scopes to add when creating a new token, separated by a comma",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "ssh-key",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "Path to a SSH key/certificate to use, overrides auto-discovery",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "insecure",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "Disable TLS verification",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "ssh-agent-principal",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Use SSH certificate with specified principal to login (needs a running ssh-agent with certificate loaded)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "ssh-agent-key",
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Usage: "Use SSH public key or SSH fingerprint to login (needs a running ssh-agent with ssh key loaded)",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "helper",
|
||||||
|
Aliases: []string{"j"},
|
||||||
|
Usage: "Add helper",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "oauth",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "Use interactive OAuth2 flow for authentication",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "client-id",
|
||||||
|
Usage: "OAuth client ID (for use with --oauth)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "redirect-url",
|
||||||
|
Usage: "OAuth redirect URL (for use with --oauth)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: runLoginAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLoginAdd(_ context.Context, cmd *cli.Command) error {
|
||||||
|
// if no args create login interactive
|
||||||
|
if cmd.NumFlags() == 0 {
|
||||||
|
if err := interact.CreateLogin(); err != nil && !interact.IsQuitting(err) {
|
||||||
|
return fmt.Errorf("error adding login: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if OAuth flag is provided, use OAuth2 PKCE flow
|
||||||
|
if cmd.Bool("oauth") {
|
||||||
|
opts := auth.OAuthOptions{
|
||||||
|
Name: cmd.String("name"),
|
||||||
|
URL: cmd.String("url"),
|
||||||
|
Insecure: cmd.Bool("insecure"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only set clientID if provided
|
||||||
|
if cmd.String("client-id") != "" {
|
||||||
|
opts.ClientID = cmd.String("client-id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only set redirect URL if provided
|
||||||
|
if cmd.String("redirect-url") != "" {
|
||||||
|
opts.RedirectURL = cmd.String("redirect-url")
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.OAuthLoginWithFullOptions(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshAgent := false
|
||||||
|
if cmd.String("ssh-agent-key") != "" || cmd.String("ssh-agent-principal") != "" {
|
||||||
|
sshAgent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// else use args to add login
|
||||||
|
return task.CreateLogin(
|
||||||
|
cmd.String("name"),
|
||||||
|
cmd.String("token"),
|
||||||
|
cmd.String("user"),
|
||||||
|
cmd.String("password"),
|
||||||
|
cmd.String("otp"),
|
||||||
|
cmd.String("scopes"),
|
||||||
|
cmd.String("ssh-key"),
|
||||||
|
cmd.String("url"),
|
||||||
|
cmd.String("ssh-agent-principal"),
|
||||||
|
cmd.String("ssh-agent-key"),
|
||||||
|
cmd.Bool("insecure"),
|
||||||
|
sshAgent,
|
||||||
|
!cmd.Bool("no-version-check"),
|
||||||
|
cmd.Bool("helper"),
|
||||||
|
)
|
||||||
|
}
|
||||||
38
cmd/login/default.go
Normal file
38
cmd/login/default.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLoginSetDefault represents to login a gitea server.
|
||||||
|
var CmdLoginSetDefault = cli.Command{
|
||||||
|
Name: "default",
|
||||||
|
Usage: "Get or Set Default Login",
|
||||||
|
Description: `Get or Set Default Login`,
|
||||||
|
ArgsUsage: "<Login>",
|
||||||
|
Action: runLoginSetDefault,
|
||||||
|
Flags: []cli.Flag{&flags.OutputFlag},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLoginSetDefault(_ context.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
l, err := config.GetDefaultLogin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Default Login: %s\n", l.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := cmd.Args().First()
|
||||||
|
return config.SetDefaultLogin(name)
|
||||||
|
}
|
||||||
43
cmd/login/delete.go
Normal file
43
cmd/login/delete.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLoginDelete is a command to delete a login
|
||||||
|
var CmdLoginDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "Remove a Gitea login",
|
||||||
|
Description: `Remove a Gitea login`,
|
||||||
|
ArgsUsage: "<login name>",
|
||||||
|
Action: RunLoginDelete,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunLoginDelete runs the action of a login delete command
|
||||||
|
func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
|
||||||
|
logins, err := config.GetLogins()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
|
||||||
|
if len(cmd.Args().First()) != 0 {
|
||||||
|
name = cmd.Args().First()
|
||||||
|
} else if len(logins) == 1 {
|
||||||
|
name = logins[0].Name
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("please specify a login name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.DeleteLogin(name)
|
||||||
|
}
|
||||||
40
cmd/login/edit.go
Normal file
40
cmd/login/edit.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLoginEdit represents to login a gitea server.
|
||||||
|
var CmdLoginEdit = cli.Command{
|
||||||
|
Name: "edit",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Usage: "Edit Gitea logins",
|
||||||
|
Description: `Edit Gitea logins`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: runLoginEdit,
|
||||||
|
Flags: []cli.Flag{&flags.OutputFlag},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLoginEdit(_ context.Context, _ *cli.Command) error {
|
||||||
|
if e, ok := os.LookupEnv("EDITOR"); ok && e != "" {
|
||||||
|
cmd := exec.Command(e, config.GetConfigPath())
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return open.Start(config.GetConfigPath())
|
||||||
|
}
|
||||||
145
cmd/login/helper.go
Normal file
145
cmd/login/helper.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLoginHelper represents to login a gitea helper.
|
||||||
|
var CmdLoginHelper = cli.Command{
|
||||||
|
Name: "helper",
|
||||||
|
Aliases: []string{"git-credential"},
|
||||||
|
Usage: "Git helper",
|
||||||
|
Description: `Git helper`,
|
||||||
|
Hidden: true,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "store",
|
||||||
|
Description: "Command drops",
|
||||||
|
Aliases: []string{"erase"},
|
||||||
|
Action: func(_ context.Context, _ *cli.Command) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "setup",
|
||||||
|
Description: "Setup helper to tea authenticate",
|
||||||
|
Action: func(_ context.Context, _ *cli.Command) error {
|
||||||
|
logins, err := config.GetLogins()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, login := range logins {
|
||||||
|
added, err := task.SetupHelper(login)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if added {
|
||||||
|
fmt.Printf("Added \"%s\"\n", login.Name)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\"%s\" has already been added!\n", login.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "get",
|
||||||
|
Description: "Get token to auth",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "login",
|
||||||
|
Aliases: []string{"l"},
|
||||||
|
Usage: "Use a specific login",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(_ context.Context, cmd *cli.Command) error {
|
||||||
|
wants := map[string]string{}
|
||||||
|
s := bufio.NewScanner(os.Stdin)
|
||||||
|
for s.Scan() {
|
||||||
|
line := s.Text()
|
||||||
|
if line == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, value := parts[0], parts[1]
|
||||||
|
if key == "url" {
|
||||||
|
u, err := url.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wants["protocol"] = u.Scheme
|
||||||
|
wants["host"] = u.Host
|
||||||
|
wants["path"] = u.Path
|
||||||
|
wants["username"] = u.User.Username()
|
||||||
|
wants["password"], _ = u.User.Password()
|
||||||
|
} else {
|
||||||
|
wants[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wants["host"]) == 0 {
|
||||||
|
return fmt.Errorf("hostname is required")
|
||||||
|
} else if len(wants["protocol"]) == 0 {
|
||||||
|
wants["protocol"] = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use --login flag if provided, otherwise fall back to host lookup
|
||||||
|
var userConfig *config.Login
|
||||||
|
if loginName := cmd.String("login"); loginName != "" {
|
||||||
|
var lookupErr error
|
||||||
|
userConfig, lookupErr = config.GetLoginByName(loginName)
|
||||||
|
if lookupErr != nil {
|
||||||
|
return lookupErr
|
||||||
|
}
|
||||||
|
if userConfig == nil {
|
||||||
|
return fmt.Errorf("login '%s' not found", loginName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var lookupErr error
|
||||||
|
userConfig, lookupErr = config.GetLoginByHost(wants["host"])
|
||||||
|
if lookupErr != nil {
|
||||||
|
return lookupErr
|
||||||
|
}
|
||||||
|
if userConfig == nil {
|
||||||
|
return fmt.Errorf("no login found for host '%s'", wants["host"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userConfig.GetAccessToken()) == 0 {
|
||||||
|
return fmt.Errorf("user not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
host, err := url.Parse(userConfig.URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh token if expired or near expiry (updates userConfig in place)
|
||||||
|
if err = userConfig.RefreshOAuthTokenIfNeeded(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(os.Stdout, "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n", host.Scheme, host.Host, userConfig.User, userConfig.GetAccessToken())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
34
cmd/login/list.go
Normal file
34
cmd/login/list.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLoginList represents to login a gitea server.
|
||||||
|
var CmdLoginList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List Gitea logins",
|
||||||
|
Description: `List Gitea logins`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: RunLoginList,
|
||||||
|
Flags: []cli.Flag{&flags.OutputFlag},
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunLoginList list all logins
|
||||||
|
func RunLoginList(_ context.Context, cmd *cli.Command) error {
|
||||||
|
logins, err := config.GetLogins()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return print.LoginsList(logins, cmd.String("output"))
|
||||||
|
}
|
||||||
71
cmd/login/oauth_refresh.go
Normal file
71
cmd/login/oauth_refresh.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/auth"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdLoginOAuthRefresh represents a command to refresh an OAuth token
|
||||||
|
var CmdLoginOAuthRefresh = cli.Command{
|
||||||
|
Name: "oauth-refresh",
|
||||||
|
Usage: "Refresh an OAuth token",
|
||||||
|
Description: "Manually refresh an expired OAuth token. If the refresh token is also expired, opens a browser for re-authentication.",
|
||||||
|
ArgsUsage: "[<login name>]",
|
||||||
|
Action: runLoginOAuthRefresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLoginOAuthRefresh(_ context.Context, cmd *cli.Command) error {
|
||||||
|
var loginName string
|
||||||
|
|
||||||
|
// Get login name from args or use default
|
||||||
|
if cmd.Args().Len() > 0 {
|
||||||
|
loginName = cmd.Args().First()
|
||||||
|
} else {
|
||||||
|
// Get default login
|
||||||
|
login, err := config.GetDefaultLogin()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("no login specified and no default login found: %s", err)
|
||||||
|
}
|
||||||
|
loginName = login.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the login from config
|
||||||
|
login, err := config.GetLoginByName(loginName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if login == nil {
|
||||||
|
return fmt.Errorf("login '%s' not found", loginName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the login has a refresh token
|
||||||
|
if login.GetRefreshToken() == "" {
|
||||||
|
return fmt.Errorf("login '%s' does not have a refresh token. It may have been created using a different authentication method", loginName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to refresh the token
|
||||||
|
err = auth.RefreshAccessToken(login)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("Successfully refreshed OAuth token for %s\n", loginName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh failed - fall back to browser-based re-authentication
|
||||||
|
fmt.Printf("Token refresh failed: %s\n", err)
|
||||||
|
fmt.Println("Opening browser for re-authentication...")
|
||||||
|
|
||||||
|
if err := auth.ReauthenticateLogin(login); err != nil {
|
||||||
|
return fmt.Errorf("re-authentication failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully re-authenticated %s\n", loginName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,61 +1,20 @@
|
|||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// SPDX-License-Identifier: MIT
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"code.gitea.io/tea/cmd/login"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CmdLogout represents to logout a gitea server.
|
// CmdLogout represents to logout a gitea server.
|
||||||
var CmdLogout = cli.Command{
|
var CmdLogout = cli.Command{
|
||||||
Name: "logout",
|
Name: "logout",
|
||||||
|
Category: catSetup,
|
||||||
Usage: "Log out from a Gitea server",
|
Usage: "Log out from a Gitea server",
|
||||||
Description: `Log out from a Gitea server`,
|
Description: `Log out from a Gitea server`,
|
||||||
Action: runLogout,
|
ArgsUsage: "<login name>",
|
||||||
Flags: []cli.Flag{
|
Action: login.RunLoginDelete,
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "name",
|
|
||||||
Aliases: []string{"n"},
|
|
||||||
Usage: "Login name to remove",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runLogout(ctx *cli.Context) error {
|
|
||||||
var name string
|
|
||||||
if len(os.Args) == 3 {
|
|
||||||
name = os.Args[2]
|
|
||||||
} else if ctx.IsSet("name") {
|
|
||||||
name = ctx.String("name")
|
|
||||||
} else {
|
|
||||||
return errors.New("Please specify a login name")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := loadConfig(yamlConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Unable to load config file " + yamlConfigPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var idx = -1
|
|
||||||
for i, l := range config.Logins {
|
|
||||||
if l.Name == name {
|
|
||||||
idx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if idx > -1 {
|
|
||||||
config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...)
|
|
||||||
err = saveConfig(yamlConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Unable to save config file " + yamlConfigPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
62
cmd/man.go
Normal file
62
cmd/man.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
docs "github.com/urfave/cli-docs/v3"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocRenderFlags are the flags for documentation generation, used by `./docs/docs.go` and the `generate-man-page` sub command
|
||||||
|
var DocRenderFlags = []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "out",
|
||||||
|
Usage: "Path to output docs to, otherwise prints to stdout",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmdGenerateManPage is the sub command to generate the `tea` man page
|
||||||
|
var CmdGenerateManPage = cli.Command{
|
||||||
|
Name: "man",
|
||||||
|
Usage: "Generate man page",
|
||||||
|
Hidden: true,
|
||||||
|
Flags: DocRenderFlags,
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return RenderDocs(cmd, cmd.Root(), docs.ToMan)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderDocs renders the documentation for `target` using the supplied `render` function
|
||||||
|
func RenderDocs(cmd, target *cli.Command, render func(*cli.Command) (string, error)) error {
|
||||||
|
out, err := render(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outPath := cmd.String("out")
|
||||||
|
if outPath == "" {
|
||||||
|
fmt.Print(out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.MkdirAll(filepath.Dir(outPath), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Create(outPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fi.Close()
|
||||||
|
if _, err = fi.WriteString(out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
59
cmd/milestones.go
Normal file
59
cmd/milestones.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/milestones"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdMilestones represents to operate repositories milestones.
|
||||||
|
var CmdMilestones = cli.Command{
|
||||||
|
Name: "milestones",
|
||||||
|
Aliases: []string{"milestone", "ms"},
|
||||||
|
Category: catEntities,
|
||||||
|
Usage: "List and create milestones",
|
||||||
|
Description: `List and create milestones`,
|
||||||
|
ArgsUsage: "[<milestone name>]",
|
||||||
|
Action: runMilestones,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&milestones.CmdMilestonesList,
|
||||||
|
&milestones.CmdMilestonesCreate,
|
||||||
|
&milestones.CmdMilestonesClose,
|
||||||
|
&milestones.CmdMilestonesDelete,
|
||||||
|
&milestones.CmdMilestonesReopen,
|
||||||
|
&milestones.CmdMilestonesIssues,
|
||||||
|
},
|
||||||
|
Flags: milestones.CmdMilestonesList.Flags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMilestones(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 1 {
|
||||||
|
return runMilestoneDetail(ctx, cmd, cmd.Args().First())
|
||||||
|
}
|
||||||
|
return milestones.RunMilestonesList(ctx, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMilestoneDetail(_ stdctx.Context, cmd *cli.Command, name string) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
|
||||||
|
milestone, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
print.MilestoneDetails(milestone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
32
cmd/milestones/close.go
Normal file
32
cmd/milestones/close.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package milestones
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdMilestonesClose represents a sub command of milestones to close an milestone
|
||||||
|
var CmdMilestonesClose = cli.Command{
|
||||||
|
Name: "close",
|
||||||
|
Usage: "Change state of one or more milestones to 'closed'",
|
||||||
|
Description: `Change state of one or more milestones to 'closed'`,
|
||||||
|
ArgsUsage: "<milestone name> [<milestone name>...]",
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Bool("force") {
|
||||||
|
return deleteMilestone(ctx, cmd)
|
||||||
|
}
|
||||||
|
return editMilestoneStatus(ctx, cmd, true)
|
||||||
|
},
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "delete milestone",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
89
cmd/milestones/create.go
Normal file
89
cmd/milestones/create.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package milestones
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdMilestonesCreate represents a sub command of milestones to create milestone
|
||||||
|
var CmdMilestonesCreate = cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Create an milestone on repository",
|
||||||
|
Description: `Create an milestone on repository`,
|
||||||
|
ArgsUsage: " ", // command does not accept arguments
|
||||||
|
Action: runMilestonesCreate,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "title",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Usage: "milestone title to create",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
Usage: "milestone description to create",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "deadline",
|
||||||
|
Aliases: []string{"expires", "x"},
|
||||||
|
Usage: "set milestone deadline (default is no due date)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "state",
|
||||||
|
Usage: "set milestone state (default is open)",
|
||||||
|
DefaultText: "open",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
date := ctx.String("deadline")
|
||||||
|
deadline := &time.Time{}
|
||||||
|
if date != "" {
|
||||||
|
t, err := dateparse.ParseAny(date)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deadline = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
state := gitea.StateOpen
|
||||||
|
if ctx.String("state") == "closed" {
|
||||||
|
state = gitea.StateClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsInteractiveMode() {
|
||||||
|
if err := interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo); err != nil && !interact.IsQuitting(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.CreateMilestone(
|
||||||
|
ctx.Login,
|
||||||
|
ctx.Owner,
|
||||||
|
ctx.Repo,
|
||||||
|
ctx.String("title"),
|
||||||
|
ctx.String("description"),
|
||||||
|
deadline,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
}
|
||||||
38
cmd/milestones/delete.go
Normal file
38
cmd/milestones/delete.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package milestones
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdMilestonesDelete represents a sub command of milestones to delete an milestone
|
||||||
|
var CmdMilestonesDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "delete a milestone",
|
||||||
|
Description: "delete a milestone",
|
||||||
|
ArgsUsage: "<milestone name>",
|
||||||
|
Action: deleteMilestone,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteMilestone(_ stdctx.Context, cmd *cli.Command) error {
|
||||||
|
ctx, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := ctx.Login.Client()
|
||||||
|
|
||||||
|
_, err = client.DeleteMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First())
|
||||||
|
return err
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user
