Compare commits
468 Commits
v1.0.1
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0904e5b9b4 | ||
|
|
9c267a071a | ||
|
|
6a451a2b59 | ||
|
|
ae9b02b079 | ||
|
|
fe94032f74 | ||
|
|
d330a23ce1 | ||
|
|
d349f059af | ||
|
|
bbbd08edc1 | ||
|
|
1b1b85439e | ||
|
|
14fe9010ae | ||
|
|
9224405155 | ||
|
|
c05bd1789c | ||
|
|
16732fbfde | ||
|
|
9a9f8fa25b | ||
|
|
f73e734411 | ||
|
|
888dee3b5f | ||
|
|
bd8fe49076 | ||
|
|
dbabc35e71 | ||
|
|
15f5d8e794 | ||
|
|
5586445207 | ||
|
|
9182a35f18 | ||
|
|
e1586898b2 | ||
|
|
97ee88975a | ||
|
|
f00a4c8078 | ||
|
|
a9de85d31d | ||
|
|
608cd54a68 | ||
|
|
430cc4f42a | ||
|
|
8bcb643a03 | ||
|
|
2aad4a5f97 | ||
|
|
b57b0c6e40 | ||
|
|
1c3bd436cc | ||
|
|
5ecb369dac | ||
|
|
71d16f69ff | ||
|
|
0693fbfc00 | ||
|
|
7a81cd16c5 | ||
|
|
ebbcf6fe12 | ||
|
|
3cf0e513e6 | ||
|
|
925b252927 | ||
|
|
1476bf909e | ||
|
|
f1d2f16b54 | ||
|
|
447c9b428f | ||
|
|
ca1c3f1926 | ||
|
|
43c5469f81 | ||
|
|
efbb895ebe | ||
|
|
03d79983ee | ||
|
|
17f403fbcd | ||
|
|
780cb692d6 | ||
|
|
42032fdecf | ||
|
|
a06c3ad2c0 | ||
|
|
09fe4a2ae9 | ||
|
|
021904e4e6 | ||
|
|
ec0ae5d50c | ||
|
|
7d8f9d1c46 | ||
|
|
8746fb3385 | ||
|
|
79ec33fd60 | ||
|
|
1ccdf19fae | ||
|
|
be5738243c | ||
|
|
08aae4952b | ||
|
|
f0efb615c5 | ||
|
|
608bbedee1 | ||
|
|
0475e7351f | ||
|
|
bdcc1a23e0 | ||
|
|
8a98a25d8e | ||
|
|
c99e7e1a62 | ||
|
|
3803f257fb | ||
|
|
1e3548b7e7 | ||
|
|
64214a9426 | ||
|
|
e2b2fd6e78 | ||
|
|
ccc15b9e1a | ||
|
|
74cde12677 | ||
|
|
f5476bdbb1 | ||
|
|
656efdc1c7 | ||
|
|
dbcd452758 | ||
|
|
05f0c4bbf5 | ||
|
|
5463640fe6 | ||
|
|
4e716fb0fa | ||
|
|
d87596aec4 | ||
|
|
1d65291342 | ||
|
|
d4fcba6796 | ||
|
|
374f1ba41f | ||
|
|
60db7ed5ac | ||
|
|
c1c4609e4e | ||
|
|
626bcf0547 | ||
|
|
aaf9adfbe8 | ||
|
|
b40496533b | ||
|
|
d2b2881306 | ||
|
|
5d40db629c | ||
|
|
af9998b8a7 | ||
|
|
7ed00b6e8d | ||
|
|
288226e13c | ||
|
|
fa41ddd3eb | ||
|
|
848293671b | ||
|
|
2215840363 | ||
|
|
0376029241 | ||
|
|
7b64b2ddab | ||
|
|
d76d67de23 | ||
|
|
0afab87631 | ||
|
|
c1d5983d3e | ||
|
|
d2165a5890 | ||
|
|
28a5bc313a | ||
|
|
6bdfadf4a9 | ||
|
|
ef13bbaf7d | ||
|
|
341b3a0349 | ||
|
|
2f7dc28b22 | ||
|
|
f3bf409082 | ||
|
|
0380ce269f | ||
|
|
cfdc62e7fa | ||
|
|
9cb08a3cf5 | ||
|
|
19bc2b10ae | ||
|
|
db6777d369 | ||
|
|
d21d5fd736 | ||
|
|
22f7aa6e9c | ||
|
|
e83c8afc56 | ||
|
|
c2eef171ff | ||
|
|
4b286f282a | ||
|
|
cf80e19157 | ||
|
|
a201977590 | ||
|
|
8947b711aa | ||
|
|
c0f99e8229 | ||
|
|
9084bdd863 | ||
|
|
136e6beb0f | ||
|
|
50918084bb | ||
|
|
e4134debd1 | ||
|
|
32faec00e6 | ||
|
|
d4e38cf129 | ||
|
|
522f194983 | ||
|
|
a5ac4c64fc | ||
|
|
ab462fb95f | ||
|
|
a30797425f | ||
|
|
5bd22a2f4a | ||
|
|
35548a9d4e | ||
|
|
95574a3640 | ||
|
|
831ff41754 | ||
|
|
bf24099114 | ||
|
|
80f900ebae | ||
|
|
19b3c45ca7 | ||
|
|
cd1821a7e2 | ||
|
|
e8e56da9ac | ||
|
|
ddb0287bf6 | ||
|
|
c4a3d5148d | ||
|
|
fc4f7e82f9 | ||
|
|
8894f856de | ||
|
|
f552b0a207 | ||
|
|
d7af8f96d7 | ||
|
|
946f3078d3 | ||
|
|
d4a7040c7f | ||
|
|
9ad3a07989 | ||
|
|
44827698e2 | ||
|
|
f1412142e0 | ||
|
|
c0ea3963be | ||
|
|
0602a44b27 | ||
|
|
817710dd47 | ||
|
|
12e71e5706 | ||
|
|
76057105ca | ||
|
|
f1ab906c51 | ||
|
|
8de8e11487 | ||
|
|
832477b1bc | ||
|
|
490d3771f7 | ||
|
|
d077fd084a | ||
|
|
0e6b9ea786 | ||
|
|
4f3880ff15 | ||
|
|
d181113b82 | ||
|
|
ec3a696e2d | ||
|
|
1f7837d6d6 | ||
|
|
29c6f32a3b | ||
|
|
83b6d03231 | ||
|
|
d6748284bd | ||
|
|
01d957677f | ||
|
|
fd941db246 | ||
|
|
fe5ff8e4b2 | ||
|
|
70ae6d197b | ||
|
|
ffde33bdfa | ||
|
|
5f234ce2a5 | ||
|
|
c5f8b96dda | ||
|
|
04fdeb9d8d | ||
|
|
d4b752def9 | ||
|
|
c2c27891c9 | ||
|
|
b8f70a27a5 | ||
|
|
6076c95dd1 | ||
|
|
dbe6d2ff8e | ||
|
|
252adc912d | ||
|
|
be48b32e63 | ||
|
|
847527fd6d | ||
|
|
669dad71f8 | ||
|
|
140967f002 | ||
|
|
4c12e2a4b9 | ||
|
|
7fd14bf7bd | ||
|
|
46320f9630 | ||
|
|
0642cb330c | ||
|
|
134f3e6e09 | ||
|
|
43c94d0a6c | ||
|
|
a31f64d639 | ||
|
|
9d2b830275 | ||
|
|
837d346090 | ||
|
|
be55460b63 | ||
|
|
9c645b54dc | ||
|
|
cf47532ebc | ||
|
|
4cfde304df | ||
|
|
7a9a5c8a69 | ||
|
|
3a91ac51a9 | ||
|
|
d67b278a0d | ||
|
|
23aba523b5 | ||
|
|
7eb8daffa3 | ||
|
|
55ae78208e | ||
|
|
1ec6b1a258 | ||
|
|
dc8248f8a4 | ||
|
|
5cc275b1de | ||
|
|
091f063706 | ||
|
|
05157808de | ||
|
|
9b4f6c1c32 | ||
|
|
076f940f1f | ||
|
|
d76f34ef51 | ||
|
|
3f67676059 | ||
|
|
8a0be5e9f0 | ||
|
|
442145dbd3 | ||
|
|
a36a8f4d72 | ||
|
|
42835c7f82 | ||
|
|
cf0f451c37 | ||
|
|
3576e1ee73 | ||
|
|
55f2059f71 | ||
|
|
284c0160c3 | ||
|
|
2f13d31ff0 | ||
|
|
b6dd6210ea | ||
|
|
40f4377717 | ||
|
|
a6751cec04 | ||
|
|
1da7dd3da9 | ||
|
|
13973348df | ||
|
|
e6b4448ba3 | ||
|
|
e4031b822a | ||
|
|
c5887b0f28 | ||
|
|
a195c3fabe | ||
|
|
181c8d9c99 | ||
|
|
d2329e1c26 | ||
|
|
23a7527e04 | ||
|
|
45a324b437 | ||
|
|
02a881aa32 | ||
|
|
ed096186a7 | ||
|
|
5fbab97373 | ||
|
|
78544f827b | ||
|
|
b13232f524 | ||
|
|
ceae143e78 | ||
|
|
94130da63a | ||
|
|
71d35dae8c | ||
|
|
76969a5671 | ||
|
|
7ab4ddad1c | ||
|
|
438e1a7d46 | ||
|
|
e388db311b | ||
|
|
f35b20b042 | ||
|
|
027591a3a5 | ||
|
|
e86d935175 | ||
|
|
bf647ce143 | ||
|
|
17c5e12e6e | ||
|
|
c73b7a65f5 | ||
|
|
e921dcf503 | ||
|
|
7c3dc51655 | ||
|
|
5fd19a45b7 | ||
|
|
f82ea42679 | ||
|
|
5972aa9af3 | ||
|
|
8a421b1fd7 | ||
|
|
49fa03bf42 | ||
|
|
d0960b8035 | ||
|
|
d4035d1cb1 | ||
|
|
a90a215662 | ||
|
|
de81f68d4d | ||
|
|
b6da658553 | ||
|
|
3898625ff1 | ||
|
|
3e0525b47d | ||
|
|
68bdaf0a6b | ||
|
|
400b6fd61c | ||
|
|
e2de16065a | ||
|
|
bf6f61cc69 | ||
|
|
73d05a51e3 | ||
|
|
d7d094bd8a | ||
|
|
2db0ffe69e | ||
|
|
ea8c8cdaf3 | ||
|
|
79ab69fe30 | ||
|
|
081485ecfd | ||
|
|
10644d6dd7 | ||
|
|
0a02fb3c4f | ||
|
|
2eb15f4a61 | ||
|
|
77ab60df83 | ||
|
|
f94869d2d1 | ||
|
|
d078aa30d6 | ||
|
|
7765593018 | ||
|
|
78535fb08e | ||
|
|
cc31a21192 | ||
|
|
aa591317e7 | ||
|
|
5348e8b71a | ||
|
|
27d30f1a61 | ||
|
|
31c717f579 | ||
|
|
abf3fbf0b1 | ||
|
|
bb76285762 | ||
|
|
a2412492da | ||
|
|
bb5a6b7a07 | ||
|
|
25663b5816 | ||
|
|
4faf097fb9 | ||
|
|
e08421017c | ||
|
|
d1b5498cc0 | ||
|
|
067ae5d96e | ||
|
|
abdc47e482 | ||
|
|
e7c3be5f2f | ||
|
|
da1b6164fe | ||
|
|
691fbdf1d3 | ||
|
|
2831267db1 | ||
|
|
0934d1b1ea | ||
|
|
a6832c234d | ||
|
|
8c2381103a | ||
|
|
2a80e5a81e | ||
|
|
634ac9c5af | ||
|
|
8555e888d8 | ||
|
|
8093b3372e | ||
|
|
75f0b0c51c | ||
|
|
833f8b94c2 | ||
|
|
8bc431952f | ||
|
|
521d429b58 | ||
|
|
3461003a0f | ||
|
|
2009f4cbda | ||
|
|
bcee9b76dd | ||
|
|
fda4476061 | ||
|
|
44d4863ecf | ||
|
|
af63684862 | ||
|
|
8b87be63c5 | ||
|
|
907b6f943c | ||
|
|
a8048c19f3 | ||
|
|
74ed6dc3ad | ||
|
|
991a4d64f3 | ||
|
|
1257d43e14 | ||
|
|
f9a3aa8737 | ||
|
|
f8c2903484 | ||
|
|
369972b116 | ||
|
|
7e401b9e39 | ||
|
|
1610b9f547 | ||
|
|
74bbec3bf9 | ||
|
|
fcf02e4961 | ||
|
|
3c4d5e1ed5 | ||
|
|
d2bb8ef503 | ||
|
|
d0ad7921f8 | ||
|
|
8c2c7b802f | ||
|
|
d1006150fb | ||
|
|
6dd096b7f0 | ||
|
|
64375d875b | ||
|
|
dce03c19cb | ||
|
|
302fa42980 | ||
|
|
88f45ce38c | ||
|
|
769f77900a | ||
|
|
240d0e84f0 | ||
|
|
a6f5efa0bb | ||
|
|
4a1f36c3cc | ||
|
|
27fcf8d30a | ||
|
|
87ad4961f6 | ||
|
|
64b167138f | ||
|
|
69b86378ce | ||
|
|
e698654902 | ||
|
|
b7eae783b5 | ||
|
|
327b1b7985 | ||
|
|
769e0a3ea6 | ||
|
|
cbf2a967c5 | ||
|
|
0af9a24087 | ||
|
|
63f0bb1761 | ||
|
|
e5620f07a4 | ||
|
|
742f2c0301 | ||
|
|
ecfa284662 | ||
|
|
7209917fd7 | ||
|
|
b316b2e740 | ||
|
|
f91b8c5f53 | ||
|
|
97170916a3 | ||
|
|
9f575986d8 | ||
|
|
19570f2d43 | ||
|
|
973282dae2 | ||
|
|
862948ab88 | ||
|
|
4b23e6a694 | ||
|
|
f4feeecc3a | ||
|
|
8fa88e584a | ||
|
|
56614b2cbe | ||
|
|
51d578ff33 | ||
|
|
6072b03291 | ||
|
|
8422ab542c | ||
|
|
03b45284e1 | ||
|
|
f2931468ec | ||
|
|
84b7d29d34 | ||
|
|
72bfabfada | ||
|
|
1a7fc53c98 | ||
|
|
61306fa737 | ||
|
|
79d527195d | ||
|
|
9d1bc9aac8 | ||
|
|
2d17d6bc16 | ||
|
|
6efa80a471 | ||
|
|
eb9ce39bb7 | ||
|
|
a5e07da8be | ||
|
|
c5f0d4b1a0 | ||
|
|
bdad3b259a | ||
|
|
1207bda94b | ||
|
|
dc3ff9f2ab | ||
|
|
467202d0a6 | ||
|
|
b354cf362e | ||
|
|
09dabe2ff2 | ||
|
|
980dd0bf51 | ||
|
|
70900bd167 | ||
|
|
51021585a7 | ||
|
|
4b0974ec10 | ||
|
|
7be02d9020 | ||
|
|
3c7116382f | ||
|
|
545ba2e2e6 | ||
|
|
f59672a18d | ||
|
|
341a3b571b | ||
|
|
727675dd46 | ||
|
|
05837a8d0f | ||
|
|
0c301f7b5c | ||
|
|
b7e1bccc50 | ||
|
|
aea1b2b02e | ||
|
|
787fda53ef | ||
|
|
a09a3dcabb | ||
|
|
dc50c78f48 | ||
|
|
1f07792881 | ||
|
|
96c201273e | ||
|
|
9ccc3698d5 | ||
|
|
6e5fffbd3f | ||
|
|
de8b73dd92 | ||
|
|
bf85c82087 | ||
|
|
c0904f1942 | ||
|
|
b75450ad36 | ||
|
|
527c2dd665 | ||
|
|
fa60cf0ea4 | ||
|
|
cb18941e63 | ||
|
|
6510e57758 | ||
|
|
6069abe5fd | ||
|
|
d0490c187c | ||
|
|
aae960b31f | ||
|
|
42904cb98a | ||
|
|
37eec6c9b7 | ||
|
|
937b4b5aa1 | ||
|
|
b992858883 | ||
|
|
2d1a1fce93 | ||
|
|
6f4ba6884c | ||
|
|
ac51caa517 | ||
|
|
799d0c2030 | ||
|
|
22e1bd31c6 | ||
|
|
35d9378e4e | ||
|
|
c22f9114c7 | ||
|
|
331316894e | ||
|
|
f686a32eac | ||
|
|
c463b1b8cb | ||
|
|
9fae9f0dc2 | ||
|
|
8f1c311b90 | ||
|
|
edfa76d3f5 | ||
|
|
a0f5471e21 | ||
|
|
ba85f68ea8 | ||
|
|
7802699f52 | ||
|
|
ba134bd27a | ||
|
|
59f736d54c | ||
|
|
98b0688921 | ||
|
|
6388761129 | ||
|
|
71dee6b7c0 | ||
|
|
71634452e1 | ||
|
|
2342df183b | ||
|
|
2e7ccecfe6 | ||
|
|
4b7594d9fa | ||
|
|
d4924d45d6 | ||
|
|
8de8ec027d | ||
|
|
9847b38518 | ||
|
|
fa3abc22c0 | ||
|
|
c1e92eeb72 | ||
|
|
a345a03d99 | ||
|
|
f27d87d93b | ||
|
|
d0932ef147 | ||
|
|
8a4161c723 | ||
|
|
25b5ffb6af | ||
|
|
f2ff0ee846 |
25
.drone.yml
25
.drone.yml
@@ -11,8 +11,7 @@ pipeline:
|
||||
image: webhippie/golang:edge
|
||||
pull: true
|
||||
environment:
|
||||
CGO_ENABLED: 1
|
||||
TAGS: sqlite bindata
|
||||
TAGS: bindata sqlite
|
||||
GOPATH: /srv/app
|
||||
commands:
|
||||
- apk -U add openssh-client
|
||||
@@ -29,38 +28,34 @@ pipeline:
|
||||
image: webhippie/golang:edge
|
||||
pull: true
|
||||
environment:
|
||||
CGO_ENABLED: 1
|
||||
TAGS: sqlite bindata
|
||||
TAGS: bindata
|
||||
GOPATH: /srv/app
|
||||
commands:
|
||||
- make test-mysql
|
||||
when:
|
||||
event: [ push ]
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
test-pgsql:
|
||||
image: webhippie/golang:edge
|
||||
pull: true
|
||||
environment:
|
||||
CGO_ENABLED: 1
|
||||
TAGS: sqlite bindata
|
||||
TAGS: bindata
|
||||
GOPATH: /srv/app
|
||||
commands:
|
||||
- make test-pgsql
|
||||
when:
|
||||
event: [ push ]
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
updater:
|
||||
static:
|
||||
image: karalabe/xgo-latest:latest
|
||||
pull: true
|
||||
environment:
|
||||
CGO_ENABLED: 1
|
||||
TAGS: sqlite bindata
|
||||
TAGS: bindata sqlite
|
||||
GOPATH: /srv/app
|
||||
commands:
|
||||
- make release
|
||||
when:
|
||||
event: [ push, tag ]
|
||||
branch: [ master, release/*, refs/tags/* ]
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
coverage:
|
||||
image: plugins/coverage
|
||||
@@ -140,11 +135,11 @@ services:
|
||||
- MYSQL_DATABASE=test
|
||||
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
|
||||
when:
|
||||
event: [ push ]
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
pgsql:
|
||||
image: postgres:9.5
|
||||
environment:
|
||||
- POSTGRES_DB=test
|
||||
when:
|
||||
event: [ push ]
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
@@ -1 +1 @@
|
||||
eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9zcnYvYXBwCiAgcGF0aDogc3JjL2NvZGUuZ2l0ZWEuaW8vZ2l0ZWEKCnBpcGVsaW5lOgogIGNsb25lOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0CiAgICB0YWdzOiB0cnVlCgogIHRlc3Q6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQ0dPX0VOQUJMRUQ6IDEKICAgICAgVEFHUzogc3FsaXRlIGJpbmRhdGEKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gYXBrIC1VIGFkZCBvcGVuc3NoLWNsaWVudAogICAgICAtIG1ha2UgY2xlYW4KICAgICAgLSBtYWtlIGdlbmVyYXRlCiAgICAgIC0gbWFrZSB2ZXQKICAgICAgLSBtYWtlIGxpbnQKICAgICAgLSBtYWtlIHRlc3QKICAgICAgLSBtYWtlIGJ1aWxkCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIHRlc3QtbXlzcWw6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQ0dPX0VOQUJMRUQ6IDEKICAgICAgVEFHUzogc3FsaXRlIGJpbmRhdGEKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gbWFrZSB0ZXN0LW15c3FsCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KCiAgdGVzdC1wZ3NxbDoKICAgIGltYWdlOiB3ZWJoaXBwaWUvZ29sYW5nOmVkZ2UKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBDR09fRU5BQkxFRDogMQogICAgICBUQUdTOiBzcWxpdGUgYmluZGF0YQogICAgICBHT1BBVEg6IC9zcnYvYXBwCiAgICBjb21tYW5kczoKICAgICAgLSBtYWtlIHRlc3QtcGdzcWwKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQoKICB1cGRhdGVyOgogICAgaW1hZ2U6IGthcmFsYWJlL3hnby1sYXRlc3Q6bGF0ZXN0CiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQ0dPX0VOQUJMRUQ6IDEKICAgICAgVEFHUzogc3FsaXRlIGJpbmRhdGEKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gbWFrZSByZWxlYXNlCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcgXQogICAgICBicmFuY2g6IFsgbWFzdGVyLCByZWxlYXNlLyosIHJlZnMvdGFncy8qIF0KCiAgY292ZXJhZ2U6CiAgICBpbWFnZTogcGx1Z2lucy9jb3ZlcmFnZQogICAgc2VydmVyOiBodHRwczovL2NvdmVyYWdlLmdpdGVhLmlvCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIGRvY2tlcjoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogZ2l0ZWEvZ2l0ZWEKICAgIHRhZ3M6IFsgJyR7RFJPTkVfVEFHIyN2fScgXQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlZnMvdGFncy8qIF0KCiAgZG9ja2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG9ja2VyCiAgICByZXBvOiBnaXRlYS9naXRlYQogICAgdGFnczogWyAnJHtEUk9ORV9CUkFOQ0gjI3JlbGVhc2Uvdn0nIF0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQogICAgICBicmFuY2g6IFsgcmVsZWFzZS8qIF0KCiAgZG9ja2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG9ja2VyCiAgICByZXBvOiBnaXRlYS9naXRlYQogICAgdGFnczogWyAnbGF0ZXN0JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc3RyaXBfcHJlZml4OiBkaXN0L3JlbGVhc2UvCiAgICBzb3VyY2U6IGRpc3QvcmVsZWFzZS8qCiAgICB0YXJnZXQ6IC9naXRlYS8ke0RST05FX1RBRyMjdn0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWZzL3RhZ3MvKiBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc3RyaXBfcHJlZml4OiBkaXN0L3JlbGVhc2UvCiAgICBzb3VyY2U6IGRpc3QvcmVsZWFzZS8qCiAgICB0YXJnZXQ6IC9naXRlYS8ke0RST05FX0JSQU5DSCMjcmVsZWFzZS92fQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCBdCiAgICAgIGJyYW5jaDogWyByZWxlYXNlLyogXQoKICByZWxlYXNlOgogICAgaW1hZ2U6IHBsdWdpbnMvczMKICAgIHBhdGhfc3R5bGU6IHRydWUKICAgIHN0cmlwX3ByZWZpeDogZGlzdC9yZWxlYXNlLwogICAgc291cmNlOiBkaXN0L3JlbGVhc2UvKgogICAgdGFyZ2V0OiAvZ2l0ZWEvbWFzdGVyCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIGdpdGh1YjoKICAgIGltYWdlOiBwbHVnaW5zL2dpdGh1Yi1yZWxlYXNlCiAgICBmaWxlczoKICAgICAgLSBkaXN0L3JlbGVhc2UvKgogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlZnMvdGFncy8qIF0KCiAgZ2l0dGVyOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0dGVyCgpzZXJ2aWNlczoKICBteXNxbDoKICAgIGltYWdlOiBteXNxbDo1LjcKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX0RBVEFCQVNFPXRlc3QKICAgICAgLSBNWVNRTF9BTExPV19FTVBUWV9QQVNTV09SRD15ZXMKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQoKICBwZ3NxbDoKICAgIGltYWdlOiBwb3N0Z3Jlczo5LjUKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPXRlc3QKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQo.NGE3UiNBappXiPimJXv1DzgjT3k2hofGPsCPhw7KsSM
|
||||
eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9zcnYvYXBwCiAgcGF0aDogc3JjL2NvZGUuZ2l0ZWEuaW8vZ2l0ZWEKCnBpcGVsaW5lOgogIGNsb25lOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0CiAgICB0YWdzOiB0cnVlCgogIHRlc3Q6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogYmluZGF0YSBzcWxpdGUKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gYXBrIC1VIGFkZCBvcGVuc3NoLWNsaWVudAogICAgICAtIG1ha2UgY2xlYW4KICAgICAgLSBtYWtlIGdlbmVyYXRlCiAgICAgIC0gbWFrZSB2ZXQKICAgICAgLSBtYWtlIGxpbnQKICAgICAgLSBtYWtlIHRlc3QKICAgICAgLSBtYWtlIGJ1aWxkCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIHRlc3QtbXlzcWw6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogYmluZGF0YQogICAgICBHT1BBVEg6IC9zcnYvYXBwCiAgICBjb21tYW5kczoKICAgICAgLSBtYWtlIHRlc3QtbXlzcWwKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgdGVzdC1wZ3NxbDoKICAgIGltYWdlOiB3ZWJoaXBwaWUvZ29sYW5nOmVkZ2UKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhCiAgICAgIEdPUEFUSDogL3Nydi9hcHAKICAgIGNvbW1hbmRzOgogICAgICAtIG1ha2UgdGVzdC1wZ3NxbAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICBzdGF0aWM6CiAgICBpbWFnZToga2FyYWxhYmUveGdvLWxhdGVzdDpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhIHNxbGl0ZQogICAgICBHT1BBVEg6IC9zcnYvYXBwCiAgICBjb21tYW5kczoKICAgICAgLSBtYWtlIHJlbGVhc2UKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgY292ZXJhZ2U6CiAgICBpbWFnZTogcGx1Z2lucy9jb3ZlcmFnZQogICAgc2VydmVyOiBodHRwczovL2NvdmVyYWdlLmdpdGVhLmlvCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIGRvY2tlcjoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogZ2l0ZWEvZ2l0ZWEKICAgIHRhZ3M6IFsgJyR7RFJPTkVfVEFHIyN2fScgXQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlZnMvdGFncy8qIF0KCiAgZG9ja2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG9ja2VyCiAgICByZXBvOiBnaXRlYS9naXRlYQogICAgdGFnczogWyAnJHtEUk9ORV9CUkFOQ0gjI3JlbGVhc2Uvdn0nIF0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQogICAgICBicmFuY2g6IFsgcmVsZWFzZS8qIF0KCiAgZG9ja2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG9ja2VyCiAgICByZXBvOiBnaXRlYS9naXRlYQogICAgdGFnczogWyAnbGF0ZXN0JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc3RyaXBfcHJlZml4OiBkaXN0L3JlbGVhc2UvCiAgICBzb3VyY2U6IGRpc3QvcmVsZWFzZS8qCiAgICB0YXJnZXQ6IC9naXRlYS8ke0RST05FX1RBRyMjdn0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWZzL3RhZ3MvKiBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc3RyaXBfcHJlZml4OiBkaXN0L3JlbGVhc2UvCiAgICBzb3VyY2U6IGRpc3QvcmVsZWFzZS8qCiAgICB0YXJnZXQ6IC9naXRlYS8ke0RST05FX0JSQU5DSCMjcmVsZWFzZS92fQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCBdCiAgICAgIGJyYW5jaDogWyByZWxlYXNlLyogXQoKICByZWxlYXNlOgogICAgaW1hZ2U6IHBsdWdpbnMvczMKICAgIHBhdGhfc3R5bGU6IHRydWUKICAgIHN0cmlwX3ByZWZpeDogZGlzdC9yZWxlYXNlLwogICAgc291cmNlOiBkaXN0L3JlbGVhc2UvKgogICAgdGFyZ2V0OiAvZ2l0ZWEvbWFzdGVyCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIGdpdGh1YjoKICAgIGltYWdlOiBwbHVnaW5zL2dpdGh1Yi1yZWxlYXNlCiAgICBmaWxlczoKICAgICAgLSBkaXN0L3JlbGVhc2UvKgogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlZnMvdGFncy8qIF0KCiAgZ2l0dGVyOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0dGVyCgpzZXJ2aWNlczoKICBteXNxbDoKICAgIGltYWdlOiBteXNxbDo1LjcKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX0RBVEFCQVNFPXRlc3QKICAgICAgLSBNWVNRTF9BTExPV19FTVBUWV9QQVNTV09SRD15ZXMKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgcGdzcWw6CiAgICBpbWFnZTogcG9zdGdyZXM6OS41CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj10ZXN0CiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCg.hp6IsxbFIQOaxJdmGv32Vf34-Nra3KqVIWzH52W687I
|
||||
@@ -25,6 +25,3 @@ indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
conf/* linguist-vendored
|
||||
docker/* linguist-vendored
|
||||
options/* linguist-vendored
|
||||
public/* linguist-vendored
|
||||
scripts/* linguist-vendored
|
||||
templates/* linguist-vendored
|
||||
16
.github/issue_template.md
vendored
16
.github/issue_template.md
vendored
@@ -1,9 +1,9 @@
|
||||
1. Please speak English, this is the language everybody of us can speak and write.
|
||||
2. Please ask questions or config/deploy problems on our Gitter channel: https://gitter.im/go-gitea/gitea
|
||||
3. Please take a moment to search that an issue doesn't already exist.
|
||||
4. Please give all relevant information below for bug reports, incomplete details will be handled as an invalid report.
|
||||
1. Please speak English, this is the language all of us can speak and write.
|
||||
2. Please ask questions or configuration/deploy problems on our Gitter channel: https://gitter.im/go-gitea/gitea
|
||||
3. Please take a moment to search check that your issue doesn't already exist.
|
||||
4. Please give all relevant information below for bug reports, because incomplete details will be handled as an invalid report.
|
||||
|
||||
**You MUST delete the content above including this line before posting, otherwise your pull request will be invalid.**
|
||||
**You MUST delete the content above including this line before posting, otherwise your issue will be invalid.**
|
||||
|
||||
- Gitea version (or commit ref):
|
||||
- Git version:
|
||||
@@ -11,6 +11,7 @@
|
||||
- Database (use `[x]`):
|
||||
- [ ] PostgreSQL
|
||||
- [ ] MySQL
|
||||
- [ ] MSSQL
|
||||
- [ ] SQLite
|
||||
- Can you reproduce the bug at https://try.gitea.io:
|
||||
- [ ] Yes (provide example URL)
|
||||
@@ -21,3 +22,8 @@
|
||||
## Description
|
||||
|
||||
...
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
**If this issue involves the Web Interface, please include a screenshot**
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,5 +41,6 @@ coverage.out
|
||||
/dist
|
||||
/custom
|
||||
/data
|
||||
/indexers
|
||||
/log
|
||||
/public/img/avatar
|
||||
|
||||
108
CHANGELOG.md
108
CHANGELOG.md
@@ -1,9 +1,115 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
* BREAKING
|
||||
* Password reset URL changed from `/user/forget_password` to `/user/forgot_password`
|
||||
|
||||
## [1.1.0](https://github.com/go-gitea/gitea/releases/tag/v1.1.0) - 2017-03-09
|
||||
|
||||
* BREAKING
|
||||
* The SSH keys can potentially break, make sure to regenerate the authorized keys
|
||||
* FEATURE
|
||||
* Git LFSv2 support [#122](https://github.com/go-gitea/gitea/pull/122)
|
||||
* API endpoints for repo watching [#191](https://github.com/go-gitea/gitea/pull/191)
|
||||
* Search within private repos [#222](https://github.com/go-gitea/gitea/pull/222)
|
||||
* Hide user email address on explore page [#336](https://github.com/go-gitea/gitea/pull/336)
|
||||
* Protected branch system [#339](https://github.com/go-gitea/gitea/pull/339)
|
||||
* Sendmail for mail delivery [#355](https://github.com/go-gitea/gitea/pull/355)
|
||||
* API endpoints for org webhooks [#372](https://github.com/go-gitea/gitea/pull/372)
|
||||
* Enabled MSSQL support [#383](https://github.com/go-gitea/gitea/pull/383)
|
||||
* API endpoints for org teams [#370](https://github.com/go-gitea/gitea/pull/370)
|
||||
* API endpoints for collaborators [#375](https://github.com/go-gitea/gitea/pull/375)
|
||||
* Graceful server restart [#416](https://github.com/go-gitea/gitea/pull/416)
|
||||
* Commitgraph / timeline on commits page [#428](https://github.com/go-gitea/gitea/pull/428)
|
||||
* API endpoints for repo forks [#509](https://github.com/go-gitea/gitea/pull/509)
|
||||
* API endpoints for releases [#510](https://github.com/go-gitea/gitea/pull/510)
|
||||
* Folder jumping [#511](https://github.com/go-gitea/gitea/pull/511)
|
||||
* Stars tab on profile page [#519](https://github.com/go-gitea/gitea/pull/519)
|
||||
* Notification system [#523](https://github.com/go-gitea/gitea/pull/523)
|
||||
* Push and pull through reverse proxy basic auth [#524](https://github.com/go-gitea/gitea/pull/524)
|
||||
* Search for issues and pull requests [#530](https://github.com/go-gitea/gitea/pull/530)
|
||||
* API endpoint for stargazers [#597](https://github.com/go-gitea/gitea/pull/597)
|
||||
* API endpoints for subscribers [#598](https://github.com/go-gitea/gitea/pull/598)
|
||||
* PID file support [#610](https://github.com/go-gitea/gitea/pull/610)
|
||||
* Two factor authentication (2FA) [#630](https://github.com/go-gitea/gitea/pull/630)
|
||||
* API endpoints for org users [#645](https://github.com/go-gitea/gitea/pull/645)
|
||||
* Release attachments [#673](https://github.com/go-gitea/gitea/pull/673)
|
||||
* OAuth2 consumer [#679](https://github.com/go-gitea/gitea/pull/679)
|
||||
* Add ability to fork your own repos [#761](https://github.com/go-gitea/gitea/pull/761)
|
||||
* Search repository on dashboard [#773](https://github.com/go-gitea/gitea/pull/773)
|
||||
* Search bar on user profile [#787](https://github.com/go-gitea/gitea/pull/787)
|
||||
* Track label changes on issue view [#788](https://github.com/go-gitea/gitea/pull/788)
|
||||
* Allow using custom time format [#798](https://github.com/go-gitea/gitea/pull/798)
|
||||
* Redirects for renamed repos [#807](https://github.com/go-gitea/gitea/pull/807)
|
||||
* Track assignee changes on issue view [#808](https://github.com/go-gitea/gitea/pull/808)
|
||||
* Track title changes on issue view [#841](https://github.com/go-gitea/gitea/pull/841)
|
||||
* Archive cleanup action [#885](https://github.com/go-gitea/gitea/pull/885)
|
||||
* Basic Open Graph support [#901](https://github.com/go-gitea/gitea/pull/901)
|
||||
* Take back control of Git hooks [#1006](https://github.com/go-gitea/gitea/pull/1006)
|
||||
* API endpoints for user repos [#1059](https://github.com/go-gitea/gitea/pull/1059)
|
||||
* BUGFIXES
|
||||
* Fixed counting issues for issue filters [#413](https://github.com/go-gitea/gitea/pull/413)
|
||||
* Added back default settings for SSH [#500](https://github.com/go-gitea/gitea/pull/500)
|
||||
* Fixed repo permissions [#513](https://github.com/go-gitea/gitea/pull/513)
|
||||
* Issues cannot be created with labels [#622](https://github.com/go-gitea/gitea/pull/622)
|
||||
* Add a reserved wiki paths check to the wiki [#720](https://github.com/go-gitea/gitea/pull/720)
|
||||
* Update website binding MaxSize to 255 [#722](https://github.com/go-gitea/gitea/pull/722)
|
||||
* User can see the private activity on public history [#818](https://github.com/go-gitea/gitea/pull/818)
|
||||
* Wrong pages number which includes private repositories [#844](https://github.com/go-gitea/gitea/pull/844)
|
||||
* Trim whitespaces for search keyword [#893](https://github.com/go-gitea/gitea/pull/893)
|
||||
* Don't rewrite non-gitea public keys [#906](https://github.com/go-gitea/gitea/pull/906)
|
||||
* Use fingerprint to check instead content for public key [#911](https://github.com/go-gitea/gitea/pull/911)
|
||||
* Fix random avatars [#1147](https://github.com/go-gitea/gitea/pull/1147)
|
||||
* ENHANCEMENT
|
||||
* Refactored process manager [#75](https://github.com/go-gitea/gitea/pull/75)
|
||||
* Restrict rights to create new orgs [#193](https://github.com/go-gitea/gitea/pull/193)
|
||||
* Added label and milestone sorting [#199](https://github.com/go-gitea/gitea/pull/199)
|
||||
* Make minimum password length configurable [#223](https://github.com/go-gitea/gitea/pull/223)
|
||||
* Speedup conflict checking on pull requests [#276](https://github.com/go-gitea/gitea/pull/276)
|
||||
* Added button to delete merged pull request branches [#441](https://github.com/go-gitea/gitea/pull/441)
|
||||
* Improved issue references within markdown [#471](https://github.com/go-gitea/gitea/pull/471)
|
||||
* Dutch translation for the landingpage [#487](https://github.com/go-gitea/gitea/pull/487)
|
||||
* Added Gogs migration script [#532](https://github.com/go-gitea/gitea/pull/532)
|
||||
* Support a .gitea folder for issue templates [#582](https://github.com/go-gitea/gitea/pull/582)
|
||||
* Enhanced diff-view coloring [#584](https://github.com/go-gitea/gitea/pull/584)
|
||||
* Added ETag header to avatars [#721](https://github.com/go-gitea/gitea/pull/721)
|
||||
* Added option to config to disable local path imports [#724](https://github.com/go-gitea/gitea/pull/724)
|
||||
* Allow custom public files [#782](https://github.com/go-gitea/gitea/pull/782)
|
||||
* Added pprof endpoint for debugging [#801](https://github.com/go-gitea/gitea/pull/801)
|
||||
* Added `X-GitHub-*` headers [#809](https://github.com/go-gitea/gitea/pull/809)
|
||||
* Fill SSH key title automatically [#863](https://github.com/go-gitea/gitea/pull/863)
|
||||
* Display Git version on admin panel [#921](https://github.com/go-gitea/gitea/pull/921)
|
||||
* Expose URL field on issue API [#982](https://github.com/go-gitea/gitea/pull/982)
|
||||
* Statically compile the binaries [#985](https://github.com/go-gitea/gitea/pull/985)
|
||||
* Embed build tags into version string [#1051](https://github.com/go-gitea/gitea/pull/1051)
|
||||
* Gitignore support for FSharp and Clojure [#1072](https://github.com/go-gitea/gitea/pull/1072)
|
||||
* Custom templates for static builds [#1087](https://github.com/go-gitea/gitea/pull/1087)
|
||||
* Add ProxyFromEnvironment if none set [#1096](https://github.com/go-gitea/gitea/pull/1096)
|
||||
* MISC
|
||||
* Replaced remaining Gogs references
|
||||
* Added more tests on various packages
|
||||
* Use Crowdin for translations again
|
||||
* Resolved some XSS attack vectors
|
||||
* Optimized and reduced number of database queries
|
||||
|
||||
## [1.0.2](https://github.com/go-gitea/gitea/releases/tag/v1.0.2) - 2017-02-21
|
||||
|
||||
* BUGFIXES
|
||||
* Fixed issue counter [#882](https://github.com/go-gitea/gitea/pull/882)
|
||||
* Fixed XSS vulnerability on wiki page [#955](https://github.com/go-gitea/gitea/pull/955)
|
||||
* Add data dir without session to dump [#587](https://github.com/go-gitea/gitea/pull/587)
|
||||
* Fixed wiki page renaming [#958](https://github.com/go-gitea/gitea/pull/958)
|
||||
* Drop default console logger if not required [#960](https://github.com/go-gitea/gitea/pull/960)
|
||||
* Fixed docker docs link on install page [#972](https://github.com/go-gitea/gitea/pull/972)
|
||||
* Handle SetModel errors [#957](https://github.com/go-gitea/gitea/pull/957)
|
||||
* Fixed XSS vulnerability on milestones [#977](https://github.com/go-gitea/gitea/pull/977)
|
||||
* Fixed XSS vulnerability on alerts [#981](https://github.com/go-gitea/gitea/pull/981)
|
||||
|
||||
## [1.0.1](https://github.com/go-gitea/gitea/releases/tag/v1.0.1) - 2017-01-05
|
||||
|
||||
* BUGFIXES
|
||||
* Fixed localized MIN_PASSWORD_LENGTH [#501](https://github.com/go-gitea/gitea/pull/501)
|
||||
* Fixed localized `MIN_PASSWORD_LENGTH` [#501](https://github.com/go-gitea/gitea/pull/501)
|
||||
* Fixed 500 error on organization delete [#507](https://github.com/go-gitea/gitea/pull/507)
|
||||
* Ignore empty wiki repo on migrate [#544](https://github.com/go-gitea/gitea/pull/544)
|
||||
* Proper check access for forking [#563](https://github.com/go-gitea/gitea/pull/563)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
This document explains how to contribute changes to the Gitea project. It assumes you have followed the [installation instructions](https://github.com/go-gitea/docs/tree/master/en-US/installation). Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
|
||||
This document explains how to contribute changes to the Gitea project. It assumes you have followed the [installation instructions](https://docs.gitea.io/en-us/). Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
|
||||
|
||||
## Bug reports
|
||||
|
||||
@@ -10,7 +10,7 @@ Please search the issues on the issue tracker with a variety of keywords to ensu
|
||||
|
||||
If unique, [open an issue](https://github.com/go-gitea/gitea/issues/new) and answer the questions so we can understand and reproduce the problematic behavior.
|
||||
|
||||
The burden is on you to convince us that it is actually a bug in Gitea. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you. Check out [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||
To show us that the issue you are having is in Gitea itself, please write clear, concise instructions so we can reproduce the behavior (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 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 getting free help.
|
||||
|
||||
@@ -24,17 +24,44 @@ This process gives everyone a chance to validate the design, helps prevent dupli
|
||||
|
||||
## Testing redux
|
||||
|
||||
Before sending code out for review, run all the tests for the whole tree to make sure the changes don't break other usage 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://readme.drone.io/0.5/install/cli/). After that you can simply call `drone exec` within your working directory and it will try to run the test suite locally.
|
||||
Before sending code out for review, run all the tests for the whole tree to make sure the changes don't break other usage 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://readme.drone.io/usage/getting-started-cli). After that you can simply call `drone exec` 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 [govendor](http://github.com/kardianos/govendor).
|
||||
|
||||
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.
|
||||
|
||||
## Code review
|
||||
|
||||
Changes to Gitea must be reviewed before they are accepted, no matter who makes the change even if it is an owner or a maintainer. We use GitHub's pull request workflow to do that and we also use [LGTM](http://lgtm.co) to ensure every PR is reviewed by at least 2 maintainers.
|
||||
|
||||
Please try to make your pull request easy to review for us. Please read the "[How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md)" guide, it has lots of useful tips for any project you may want to contribute. Some of the key points:
|
||||
Please try to make your pull request easy to review for us. Please read the "[How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md)" guide, it has lots of useful tips for any project you may want to contribute. Some of the key points:
|
||||
|
||||
* Make small pull requests. The smaller, the faster to review and the more likely it will be merged soon.
|
||||
* Don't make changes unrelated to your PR. Maybe there are typos on some comments, maybe refactoring would be welcome on a function... but if that is not related to your PR, please make *another* PR for that.
|
||||
* Split big pull requests in multiple small ones. An incremental change will be faster to review than a huge PR.
|
||||
* Split big pull requests into multiple small ones. An incremental change will be faster to review than a huge PR.
|
||||
|
||||
## Styleguide
|
||||
|
||||
For imports you should use the following format (_without_ the comments)
|
||||
```go
|
||||
import (
|
||||
// stdlib
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
// local packages
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/sdk/gitea"
|
||||
|
||||
// external packages
|
||||
"github.com/foo/bar"
|
||||
"gopkg.io/baz.v1"
|
||||
)
|
||||
```
|
||||
|
||||
## Sign your work
|
||||
|
||||
@@ -44,20 +71,26 @@ The sign-off is a simple line at the end of the explanation for the patch. Your
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
```
|
||||
|
||||
Please use your real name, we really dislike pseudonyms or anonymous contributions. We are in the opensource world without secrets. If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
Please use your real name, we really dislike pseudonyms or anonymous contributions. We are in the open-source world without secrets. If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
|
||||
## Release Cycle
|
||||
|
||||
We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. The overall goal is to make a major 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. A release is maintained by issuing minor releases to only correct critical problems such as crashes or security issues. All the feature pull requests should be merged in the first month of one release period.
|
||||
|
||||
The current release cycle is aligned to start on December 25 to February 24, next is February 25 to April 24, and etc. On this cycle, we also maybe publish the previous release minor version. For example, the current release version is v1.1, but we maybe also publish v1.0.2. When we publish v1.2, then we will stop publish v1.0.3.
|
||||
|
||||
## Maintainers
|
||||
|
||||
To make sure every PR is checked, we got team maintainers. Every PR **MUST** be reviewed by at least two maintainers (or owners) before it can get merged. A maintainer should be a contributor of Gitea (or Gogs) and contributed at least 4 accepted PRs. A contributor should apply as a maintainer in the [Gitter develop channel](https://gitter.im/go-gitea/develop). The owners or the team maintainers may invite the contributor. A maintainer should spend some time on code reviews. If a maintainer has no time to do that, they should apply to leave the maintainers team and we will give them the honor of being a member of the advisors team. Of course, if an advisor has time to code review, we will gladly welcome them back to maintainers team. If someone has no time to code review and forgets to leave the maintainers team, the owners have the power to move him from maintainers team to advisors team.
|
||||
To make sure every PR is checked, we have [team maintainers](https://github.com/orgs/go-gitea/teams/maintainers). Every PR **MUST** be reviewed by at least two maintainers (or owners) before it can get merged. A maintainer should be a contributor of Gitea (or Gogs) and contributed at least 4 accepted PRs. A contributor should apply as a maintainer in the [Gitter develop channel](https://gitter.im/go-gitea/develop). The owners or the team maintainers may invite the contributor. A maintainer should spend some time on code reviews. If a maintainer has no time to do that, they should apply to leave the maintainers team and we will give them the honor of being a member of the [advisors team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if an advisor has time to code review, we will gladly welcome them back to the maintainers team. If a maintainer is inactive for more than 3 months and forgets to leave the maintainers team, the owners may move him or her from the maintainers team to the advisors team.
|
||||
|
||||
## Owners
|
||||
|
||||
Since Gitea is a pure community organization without any company support, to keep the development healthy we will elect the owners every year. Every time we will elect three owners. All the contributors may vote up to three people, one of which is the main owner, and the others are assistant owners. When the new owners have been elected, the old owners MUST move the power to the new ones. If an owner don't obey these rules, the others are allowed to revoke his owner status.
|
||||
Since Gitea is a pure community organization without any company support, 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 be the main owner, and the other two the assistant owners. When the new owners have been elected, the old owners will give up ownership to the newly elected owners. If an owner is unable to do so, the other owners will assist in ceding ownership to the newly elected owners.
|
||||
|
||||
After the election, the new owners should say they agree with these rules on the [CONTRIBUTING](CONTRIBUTING.md) on the [Gitter main channel](https://gitter.im/go-gitea/gitea). Below are the words to speak:
|
||||
After the election, the new owners should proactively agree with our [CONTRIBUTING](CONTRIBUTING.md) requirements on the [Gitter main channel](https://gitter.im/go-gitea/gitea). Below are the words to speak:
|
||||
|
||||
```
|
||||
I'm glad to be an owner of Gitea, I agree with [CONTRIBUTING](CONTRIBUTING.md). I will spend part of my time on Gitea and lead the development of Gitea.
|
||||
I'm honored to having been elected an owner of Gitea, I agree with [CONTRIBUTING](CONTRIBUTING.md). I will spend part of my time on Gitea and lead the development of Gitea.
|
||||
```
|
||||
|
||||
To honor the past owners, here's the history of the owners and the time they served:
|
||||
@@ -78,7 +111,7 @@ Since the `master` branch is a tip version, if you wish to use Gitea in producti
|
||||
Code that you contribute should use the standard copyright header:
|
||||
|
||||
```
|
||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2017 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.
|
||||
```
|
||||
|
||||
43
Dockerfile.aarch64
Normal file
43
Dockerfile.aarch64
Normal file
@@ -0,0 +1,43 @@
|
||||
FROM aarch64/alpine:3.5
|
||||
|
||||
EXPOSE 22 3000
|
||||
|
||||
RUN apk update && \
|
||||
apk add \
|
||||
su-exec \
|
||||
ca-certificates \
|
||||
sqlite \
|
||||
bash \
|
||||
git \
|
||||
linux-pam \
|
||||
s6 \
|
||||
curl \
|
||||
openssh \
|
||||
tzdata && \
|
||||
rm -rf \
|
||||
/var/cache/apk/* && \
|
||||
addgroup \
|
||||
-S -g 1000 \
|
||||
git && \
|
||||
adduser \
|
||||
-S -H -D \
|
||||
-h /data/git \
|
||||
-s /bin/bash \
|
||||
-u 1000 \
|
||||
-G git \
|
||||
git && \
|
||||
echo "git:$(date +%s | sha256sum | base64 | head -c 32)" | chpasswd
|
||||
|
||||
ENV USER git
|
||||
ENV GITEA_CUSTOM /data/gitea
|
||||
|
||||
COPY docker /
|
||||
COPY gitea /app/gitea/gitea
|
||||
|
||||
ENV GODEBUG=netdns=go
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/entrypoint"]
|
||||
CMD ["/bin/s6-svscan", "/etc/s6"]
|
||||
|
||||
@@ -26,7 +26,8 @@ RUN apk update && \
|
||||
-s /bin/bash \
|
||||
-u 1000 \
|
||||
-G git \
|
||||
git
|
||||
git && \
|
||||
echo "git:$(date +%s | sha256sum | base64 | head -c 32)" | chpasswd
|
||||
|
||||
ENV USER git
|
||||
ENV GITEA_CUSTOM /data/gitea
|
||||
@@ -38,7 +39,4 @@ ENTRYPOINT ["/usr/bin/entrypoint"]
|
||||
CMD ["/bin/s6-svscan", "/etc/s6"]
|
||||
|
||||
COPY docker /
|
||||
|
||||
COPY public /app/gitea/public
|
||||
COPY templates /app/gitea/templates
|
||||
COPY gitea /app/gitea/gitea
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
Alexey Makhov <amakhov@avito.ru> (@makhov)
|
||||
Andrey Nering <andrey.nering@gmail.com> (@andreynering)
|
||||
Bo-Yi Wu <appleboy.tw@gmail.com> (@appleboy)
|
||||
Ethan Koenig <ethantkoenig@gmail.com> (@ethantkoenig)
|
||||
Kees de Vries <bouwko@gmail.com> (@Bwko)
|
||||
Kim Carlbäcker <kim.carlbacker@gmail.com> (@bkcsoft)
|
||||
LefsFlare <nobody@nobody.tld> (@LefsFlarey)
|
||||
@@ -10,3 +12,4 @@ Rémy Boulanouar <admin@dblk.org> (@DblK)
|
||||
Sandro Santilli <strk@kbt.io> (@strk)
|
||||
Thibault Meyer <meyer.thibault@gmail.com> (@0xbaadf00d)
|
||||
Thomas Boerger <thomas@webhippie.de> (@tboerger)
|
||||
Patrick G <geek1011@outlook.com> (@geek1011)
|
||||
|
||||
59
Makefile
59
Makefile
@@ -1,15 +1,19 @@
|
||||
DIST := dist
|
||||
EXECUTABLE := gitea
|
||||
IMPORT := code.gitea.io/gitea
|
||||
|
||||
ifeq ($(OS), Windows_NT)
|
||||
EXECUTABLE := gitea.exe
|
||||
else
|
||||
EXECUTABLE := gitea
|
||||
endif
|
||||
|
||||
BINDATA := modules/{options,public,templates}/bindata.go
|
||||
STYLESHEETS := $(wildcard public/less/index.less public/less/_*.less)
|
||||
JAVASCRIPTS :=
|
||||
|
||||
LDFLAGS += -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')"
|
||||
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
|
||||
|
||||
TARGETS ?= linux/*,darwin/*,windows/*
|
||||
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
|
||||
PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations,$(shell go list ./... | grep -v /vendor/))
|
||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||
|
||||
TAGS ?=
|
||||
@@ -34,7 +38,7 @@ clean:
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
go fmt $(PACKAGES)
|
||||
find . -name "*.go" -type f -not -path "./vendor/*" | xargs gofmt -s -w
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
@@ -42,25 +46,30 @@ vet:
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
@which go-bindata > /dev/null; if [ $$? -ne 0 ]; then \
|
||||
@hash go-bindata > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/jteeuwen/go-bindata/...; \
|
||||
fi
|
||||
go generate $(PACKAGES)
|
||||
|
||||
.PHONY: errcheck
|
||||
errcheck:
|
||||
@which errcheck > /dev/null; if [ $$? -ne 0 ]; then \
|
||||
@hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/kisielk/errcheck; \
|
||||
fi
|
||||
errcheck $(PACKAGES)
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@which golint > /dev/null; if [ $$? -ne 0 ]; then \
|
||||
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/golang/lint/golint; \
|
||||
fi
|
||||
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
||||
|
||||
.PHONY: integrations
|
||||
integrations: TAGS=bindata sqlite
|
||||
integrations: build
|
||||
go test code.gitea.io/gitea/integrations
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
for PKG in $(PACKAGES); do go test -cover -coverprofile $$GOPATH/src/$$PKG/coverage.out $$PKG || exit 1; done;
|
||||
@@ -84,26 +93,46 @@ install: $(wildcard *.go)
|
||||
build: $(EXECUTABLE)
|
||||
|
||||
$(EXECUTABLE): $(SOURCES)
|
||||
go build -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
|
||||
go build -i -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
docker run -ti --rm -v $(CURDIR):/srv/app/src/code.gitea.io/gitea -w /srv/app/src/code.gitea.io/gitea -e TAGS="$(TAGS)" webhippie/golang:edge make clean generate build
|
||||
docker run -ti --rm -v $(CURDIR):/srv/app/src/code.gitea.io/gitea -w /srv/app/src/code.gitea.io/gitea -e TAGS="bindata $(TAGS)" webhippie/golang:edge make clean generate build
|
||||
docker build -t gitea/gitea:latest .
|
||||
|
||||
.PHONY: release
|
||||
release: release-dirs release-build release-copy release-check
|
||||
release: release-dirs release-windows release-linux release-darwin release-copy release-check
|
||||
|
||||
.PHONY: release-dirs
|
||||
release-dirs:
|
||||
mkdir -p $(DIST)/binaries $(DIST)/release
|
||||
|
||||
.PHONY: release-build
|
||||
release-build:
|
||||
@which xgo > /dev/null; if [ $$? -ne 0 ]; then \
|
||||
.PHONY: release-windows
|
||||
release-windows:
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/karalabe/xgo; \
|
||||
fi
|
||||
xgo -dest $(DIST)/binaries -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets '$(TARGETS)' -out $(EXECUTABLE)-$(VERSION) $(IMPORT)
|
||||
xgo -dest $(DIST)/binaries -tags '$(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
||||
ifeq ($(CI),drone)
|
||||
mv /build/* $(DIST)/binaries
|
||||
endif
|
||||
|
||||
.PHONY: release-linux
|
||||
release-linux:
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/karalabe/xgo; \
|
||||
fi
|
||||
xgo -dest $(DIST)/binaries -tags '$(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/*' -out gitea-$(VERSION) .
|
||||
ifeq ($(CI),drone)
|
||||
mv /build/* $(DIST)/binaries
|
||||
endif
|
||||
|
||||
.PHONY: release-darwin
|
||||
release-darwin:
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/karalabe/xgo; \
|
||||
fi
|
||||
xgo -dest $(DIST)/binaries -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out gitea-$(VERSION) .
|
||||
ifeq ($(CI),drone)
|
||||
mv /build/* $(DIST)/binaries
|
||||
endif
|
||||
|
||||
17
README.md
17
README.md
@@ -2,24 +2,23 @@
|
||||
|
||||
# Gitea - Git with a cup of tea
|
||||
|
||||
[](http://drone.gitea.io/go-gitea/gitea)
|
||||
[](https://drone.gitea.io/go-gitea/gitea)
|
||||
[](https://gitter.im/go-gitea/gitea?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://microbadger.com/images/gitea/gitea "Get your own image badge on microbadger.com")
|
||||
[](https://microbadger.com/images/gitea/gitea "Get your own image badge on microbadger.com")
|
||||
[](https://coverage.gitea.io/go-gitea/gitea)
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea)
|
||||
[](https://godoc.org/code.gitea.io/gitea)
|
||||
[](https://github.com/go-gitea/gitea/releases/latest)
|
||||
[](https://github.com/go-gitea/gitea/releases/latest)
|
||||
|
||||
||||
|
||||
|:-------------:|:-------:|:-------:|
|
||||
| | | |
|
||||
|:---:|:---:|:---:|
|
||||
||||
|
||||
||||
|
||||
||||
|
||||
||||
|
||||
|
||||
## Purpose
|
||||
|
||||
The goal of this project is to make the easiest, fastest, and most painless way of setting up a self-hosted Git service. With Go, this can be done with an independent binary distribution across **all platforms** that Go supports, including Linux, macOS, and Windows on x86, amd64, ARM and PowerPC architectures. Want to try it before doing anything else? Do it [online](https://try.gitea.io/)!
|
||||
The goal of this project is to make the easiest, fastest, and most painless way of setting up a self-hosted Git service. Using Go, this can be done with an independent binary distribution across **all platforms** which Go supports, including Linux, macOS, and Windows on x86, amd64, ARM and PowerPC architectures. Want to try it before doing anything else? Do it [with the online demo](https://try.gitea.io/)! This project has been [forked](https://blog.gitea.io/2016/12/welcome-to-gitea/) from [Gogs](https://gogs.io).
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -29,7 +28,7 @@ The goal of this project is to make the easiest, fastest, and most painless way
|
||||
|
||||
## Docs
|
||||
|
||||
For further information or instructions how to install Gitea please take a look at our [documentation](https://docs.gitea.io/en-us/), if you can not find some specific information just head over to our [Gitter](https://gitter.im/go-gitea/gitea) channel to have a chat with us.
|
||||
For more information and instructions about how to install Gitea please look at our [documentation](https://docs.gitea.io/en-us/). If you cannot find some specific information, then head over to our [Gitter](https://gitter.im/go-gitea/gitea) channel to chat with us.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -43,4 +42,4 @@ Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is under the MIT License. See the [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) file for the full license text.
|
||||
This project is licensed under the MIT License. See the [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) file for the full license text.
|
||||
|
||||
11
README_ZH.md
11
README_ZH.md
@@ -2,20 +2,19 @@
|
||||
|
||||
# Gitea - Git with a cup of tea
|
||||
|
||||
[](http://drone.gitea.io/go-gitea/gitea)
|
||||
[](https://drone.gitea.io/go-gitea/gitea)
|
||||
[](https://gitter.im/go-gitea/gitea?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://microbadger.com/images/gitea/gitea "Get your own image badge on microbadger.com")
|
||||
[](https://microbadger.com/images/gitea/gitea "Get your own image badge on microbadger.com")
|
||||
[](https://coverage.gitea.io/go-gitea/gitea)
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea)
|
||||
[](https://godoc.org/code.gitea.io/gitea)
|
||||
[](https://github.com/go-gitea/gitea/releases/latest)
|
||||
[](https://github.com/go-gitea/gitea/releases/latest)
|
||||
|
||||
||||
|
||||
|:-------------:|:-------:|:-------:|
|
||||
| | | |
|
||||
|:---:|:---:|:---:|
|
||||
||||
|
||||
||||
|
||||
||||
|
||||
||||
|
||||
|
||||
## 目标
|
||||
|
||||
|
||||
60
cmd/admin.go
60
cmd/admin.go
@@ -18,11 +18,12 @@ var (
|
||||
// CmdAdmin represents the available admin sub-command.
|
||||
CmdAdmin = cli.Command{
|
||||
Name: "admin",
|
||||
Usage: "Preform admin operations on command line",
|
||||
Usage: "Perform admin operations on command line",
|
||||
Description: `Allow using internal logic of Gitea without hacking into the source code
|
||||
to make automatic initialization process more smoothly`,
|
||||
Subcommands: []cli.Command{
|
||||
subcmdCreateUser,
|
||||
subcmdChangePassword,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,8 +58,59 @@ to make automatic initialization process more smoothly`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
subcmdChangePassword = cli.Command{
|
||||
Name: "change-password",
|
||||
Usage: "Change a user's password",
|
||||
Action: runChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "username,u",
|
||||
Value: "",
|
||||
Usage: "The user to change password for",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password,p",
|
||||
Value: "",
|
||||
Usage: "New password to set for user",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func runChangePassword(c *cli.Context) error {
|
||||
if !c.IsSet("password") {
|
||||
return fmt.Errorf("Password is not specified")
|
||||
} else if !c.IsSet("username") {
|
||||
return fmt.Errorf("Username is not specified")
|
||||
}
|
||||
|
||||
setting.NewContext()
|
||||
models.LoadConfigs()
|
||||
|
||||
setting.NewXORMLogService(false)
|
||||
if err := models.SetEngine(); err != nil {
|
||||
return fmt.Errorf("models.SetEngine: %v", err)
|
||||
}
|
||||
|
||||
uname := c.String("username")
|
||||
user, err := models.GetUserByName(uname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v", err)
|
||||
}
|
||||
user.Passwd = c.String("password")
|
||||
if user.Salt, err = models.GetUserSalt(); err != nil {
|
||||
return fmt.Errorf("%v", err)
|
||||
}
|
||||
user.EncodePasswd()
|
||||
if err := models.UpdateUser(user); err != nil {
|
||||
return fmt.Errorf("%v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User '%s' password has been successfully updated!\n", uname)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCreateUser(c *cli.Context) error {
|
||||
if !c.IsSet("name") {
|
||||
return fmt.Errorf("Username is not specified")
|
||||
@@ -74,7 +126,11 @@ func runCreateUser(c *cli.Context) error {
|
||||
|
||||
setting.NewContext()
|
||||
models.LoadConfigs()
|
||||
models.SetEngine()
|
||||
|
||||
setting.NewXORMLogService(false)
|
||||
if err := models.SetEngine(); err != nil {
|
||||
return fmt.Errorf("models.SetEngine: %v", err)
|
||||
}
|
||||
|
||||
if err := models.CreateUser(&models.User{
|
||||
Name: c.String("name"),
|
||||
|
||||
14
cmd/cert.go
14
cmd/cert.go
@@ -82,7 +82,7 @@ func pemBlockForKey(priv interface{}) *pem.Block {
|
||||
case *ecdsa.PrivateKey:
|
||||
b, err := x509.MarshalECPrivateKey(k)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to marshal ECDSA private key: %v\n", err)
|
||||
log.Fatalf("Unable to marshal ECDSA private key: %v", err)
|
||||
}
|
||||
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
|
||||
default:
|
||||
@@ -112,7 +112,7 @@ func runCert(ctx *cli.Context) error {
|
||||
log.Fatalf("Unrecognized elliptic curve: %q", ctx.String("ecdsa-curve"))
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate private key: %s", err)
|
||||
log.Fatalf("Failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
var notBefore time.Time
|
||||
@@ -121,7 +121,7 @@ func runCert(ctx *cli.Context) error {
|
||||
} else {
|
||||
notBefore, err = time.Parse("Jan 2 15:04:05 2006", ctx.String("start-date"))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse creation date: %s", err)
|
||||
log.Fatalf("Failed to parse creation date: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func runCert(ctx *cli.Context) error {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate serial number: %s", err)
|
||||
log.Fatalf("Failed to generate serial number: %v", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
@@ -163,12 +163,12 @@ func runCert(ctx *cli.Context) error {
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create certificate: %s", err)
|
||||
log.Fatalf("Failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
certOut, err := os.Create("cert.pem")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open cert.pem for writing: %s", err)
|
||||
log.Fatalf("Failed to open cert.pem for writing: %v", err)
|
||||
}
|
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
certOut.Close()
|
||||
@@ -176,7 +176,7 @@ func runCert(ctx *cli.Context) error {
|
||||
|
||||
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open key.pem for writing: %v\n", err)
|
||||
log.Fatalf("Failed to open key.pem for writing: %v", err)
|
||||
}
|
||||
pem.Encode(keyOut, pemBlockForKey(priv))
|
||||
keyOut.Close()
|
||||
|
||||
100
cmd/dump.go
100
cmd/dump.go
@@ -11,11 +11,13 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"github.com/Unknwon/cae/zip"
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
@@ -41,6 +43,10 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||
Value: os.TempDir(),
|
||||
Usage: "Temporary dir path",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "database, d",
|
||||
Usage: "Specify the database SQL syntax",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,8 +55,13 @@ func runDump(ctx *cli.Context) error {
|
||||
setting.CustomConf = ctx.String("config")
|
||||
}
|
||||
setting.NewContext()
|
||||
setting.NewServices() // cannot access session settings otherwise
|
||||
models.LoadConfigs()
|
||||
models.SetEngine()
|
||||
|
||||
err := models.SetEngine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpDir := ctx.String("tempdir")
|
||||
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
|
||||
@@ -58,7 +69,7 @@ func runDump(ctx *cli.Context) error {
|
||||
}
|
||||
TmpWorkDir, err := ioutil.TempDir(tmpDir, "gitea-dump-")
|
||||
if err != nil {
|
||||
log.Fatalf("Fail to create tmp work directory: %v", err)
|
||||
log.Fatalf("Failed to create tmp work directory: %v", err)
|
||||
}
|
||||
log.Printf("Creating tmp work dir: %s", TmpWorkDir)
|
||||
|
||||
@@ -68,42 +79,64 @@ func runDump(ctx *cli.Context) error {
|
||||
log.Printf("Dumping local repositories...%s", setting.RepoRootPath)
|
||||
zip.Verbose = ctx.Bool("verbose")
|
||||
if err := zip.PackTo(setting.RepoRootPath, reposDump, true); err != nil {
|
||||
log.Fatalf("Fail to dump local repositories: %v", err)
|
||||
log.Fatalf("Failed to dump local repositories: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Dumping database...")
|
||||
if err := models.DumpDatabase(dbDump); err != nil {
|
||||
log.Fatalf("Fail to dump database: %v", err)
|
||||
targetDBType := ctx.String("database")
|
||||
if len(targetDBType) > 0 && targetDBType != models.DbCfg.Type {
|
||||
log.Printf("Dumping database %s => %s...", models.DbCfg.Type, targetDBType)
|
||||
} else {
|
||||
log.Printf("Dumping database...")
|
||||
}
|
||||
|
||||
if err := models.DumpDatabase(dbDump, targetDBType); err != nil {
|
||||
log.Fatalf("Failed to dump database: %v", err)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix())
|
||||
log.Printf("Packing dump files...")
|
||||
z, err := zip.Create(fileName)
|
||||
if err != nil {
|
||||
log.Fatalf("Fail to create %s: %v", fileName, err)
|
||||
log.Fatalf("Failed to create %s: %v", fileName, err)
|
||||
}
|
||||
|
||||
if err := z.AddFile("gitea-repo.zip", reposDump); err != nil {
|
||||
log.Fatalf("Fail to include gitea-repo.zip: %v", err)
|
||||
log.Fatalf("Failed to include gitea-repo.zip: %v", err)
|
||||
}
|
||||
if err := z.AddFile("gitea-db.sql", dbDump); err != nil {
|
||||
log.Fatalf("Fail to include gitea-db.sql: %v", err)
|
||||
log.Fatalf("Failed to include gitea-db.sql: %v", err)
|
||||
}
|
||||
customDir, err := os.Stat(setting.CustomPath)
|
||||
if err == nil && customDir.IsDir() {
|
||||
if err := z.AddDir("custom", setting.CustomPath); err != nil {
|
||||
log.Fatalf("Fail to include custom: %v", err)
|
||||
log.Fatalf("Failed to include custom: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Custom dir %s doesn't exist, skipped", setting.CustomPath)
|
||||
}
|
||||
if err := z.AddDir("log", setting.LogRootPath); err != nil {
|
||||
log.Fatalf("Fail to include log: %v", err)
|
||||
|
||||
if com.IsExist(setting.AppDataPath) {
|
||||
log.Printf("Packing data directory...%s", setting.AppDataPath)
|
||||
|
||||
var sessionAbsPath string
|
||||
if setting.SessionConfig.Provider == "file" {
|
||||
if len(setting.SessionConfig.ProviderConfig) == 0 {
|
||||
setting.SessionConfig.ProviderConfig = "data/sessions"
|
||||
}
|
||||
sessionAbsPath, _ = filepath.Abs(setting.SessionConfig.ProviderConfig)
|
||||
}
|
||||
if err := zipAddDirectoryExclude(z, "data", setting.AppDataPath, sessionAbsPath); err != nil {
|
||||
log.Fatalf("Failed to include data directory: %v", err)
|
||||
}
|
||||
}
|
||||
// FIXME: SSH key file.
|
||||
|
||||
if err := z.AddDir("log", setting.LogRootPath); err != nil {
|
||||
log.Fatalf("Failed to include log: %v", err)
|
||||
}
|
||||
|
||||
if err = z.Close(); err != nil {
|
||||
_ = os.Remove(fileName)
|
||||
log.Fatalf("Fail to save %s: %v", fileName, err)
|
||||
log.Fatalf("Failed to save %s: %v", fileName, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(fileName, 0600); err != nil {
|
||||
@@ -113,9 +146,46 @@ func runDump(ctx *cli.Context) error {
|
||||
log.Printf("Removing tmp work dir: %s", TmpWorkDir)
|
||||
|
||||
if err := os.RemoveAll(TmpWorkDir); err != nil {
|
||||
log.Fatalf("Fail to remove %s: %v", TmpWorkDir, err)
|
||||
log.Fatalf("Failed to remove %s: %v", TmpWorkDir, err)
|
||||
}
|
||||
log.Printf("Finish dumping in file %s", fileName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// zipAddDirectoryExclude zips absPath to specified zipPath inside z excluding excludeAbsPath
|
||||
func zipAddDirectoryExclude(zip *zip.ZipArchive, zipPath, absPath string, excludeAbsPath string) error {
|
||||
absPath, err := filepath.Abs(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
zip.AddEmptyDir(zipPath)
|
||||
|
||||
files, err := dir.Readdir(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
currentAbsPath := path.Join(absPath, file.Name())
|
||||
currentZipPath := path.Join(zipPath, file.Name())
|
||||
if file.IsDir() {
|
||||
if currentAbsPath != excludeAbsPath {
|
||||
if err = zipAddDirectoryExclude(zip, currentZipPath, currentAbsPath, excludeAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if err = zip.AddFile(currentZipPath, currentAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
233
cmd/hook.go
Normal file
233
cmd/hook.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright 2017 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
// CmdHook represents the available hooks sub-command.
|
||||
CmdHook = cli.Command{
|
||||
Name: "hook",
|
||||
Usage: "Delegate commands to corresponding Git hooks",
|
||||
Description: "This should only be called by Git",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config, c",
|
||||
Value: "custom/conf/app.ini",
|
||||
Usage: "Custom configuration file path",
|
||||
},
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
subcmdHookPreReceive,
|
||||
subcmdHookUpadte,
|
||||
subcmdHookPostReceive,
|
||||
},
|
||||
}
|
||||
|
||||
subcmdHookPreReceive = cli.Command{
|
||||
Name: "pre-receive",
|
||||
Usage: "Delegate pre-receive Git hook",
|
||||
Description: "This command should only be called by Git",
|
||||
Action: runHookPreReceive,
|
||||
}
|
||||
subcmdHookUpadte = cli.Command{
|
||||
Name: "update",
|
||||
Usage: "Delegate update Git hook",
|
||||
Description: "This command should only be called by Git",
|
||||
Action: runHookUpdate,
|
||||
}
|
||||
subcmdHookPostReceive = cli.Command{
|
||||
Name: "post-receive",
|
||||
Usage: "Delegate post-receive Git hook",
|
||||
Description: "This command should only be called by Git",
|
||||
Action: runHookPostReceive,
|
||||
}
|
||||
)
|
||||
|
||||
func runHookPreReceive(c *cli.Context) error {
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.IsSet("config") {
|
||||
setting.CustomConf = c.String("config")
|
||||
} else if c.GlobalIsSet("config") {
|
||||
setting.CustomConf = c.GlobalString("config")
|
||||
}
|
||||
|
||||
if err := setup("hooks/pre-receive.log"); err != nil {
|
||||
fail("Hook pre-receive init failed", fmt.Sprintf("setup: %v", err))
|
||||
}
|
||||
|
||||
// the environment setted on serv command
|
||||
repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64)
|
||||
isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
|
||||
//username := os.Getenv(models.EnvRepoUsername)
|
||||
//reponame := os.Getenv(models.EnvRepoName)
|
||||
//repoPath := models.RepoPath(username, reponame)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
buf.Write(scanner.Bytes())
|
||||
buf.WriteByte('\n')
|
||||
|
||||
// TODO: support news feeds for wiki
|
||||
if isWiki {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
//oldCommitID := string(fields[0])
|
||||
newCommitID := string(fields[1])
|
||||
refFullName := string(fields[2])
|
||||
|
||||
// FIXME: when we add feature to protected branch to deny force push, then uncomment below
|
||||
/*var isForce bool
|
||||
// detect force push
|
||||
if git.EmptySHA != oldCommitID {
|
||||
output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).RunInDir(repoPath)
|
||||
if err != nil {
|
||||
fail("Internal error", "Fail to detect force push: %v", err)
|
||||
} else if len(output) > 0 {
|
||||
isForce = true
|
||||
}
|
||||
}*/
|
||||
|
||||
branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
|
||||
protectBranch, err := models.GetProtectedBranchBy(repoID, branchName)
|
||||
if err != nil {
|
||||
log.GitLogger.Fatal(2, "retrieve protected branches information failed")
|
||||
}
|
||||
|
||||
if protectBranch != nil {
|
||||
// check and deletion
|
||||
if newCommitID == git.EmptySHA {
|
||||
fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "")
|
||||
} else {
|
||||
fail(fmt.Sprintf("protected branch %s can not be pushed to", branchName), "")
|
||||
//fail(fmt.Sprintf("branch %s is protected from force push", branchName), "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHookUpdate(c *cli.Context) error {
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.IsSet("config") {
|
||||
setting.CustomConf = c.String("config")
|
||||
} else if c.GlobalIsSet("config") {
|
||||
setting.CustomConf = c.GlobalString("config")
|
||||
}
|
||||
|
||||
if err := setup("hooks/update.log"); err != nil {
|
||||
fail("Hook update init failed", fmt.Sprintf("setup: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHookPostReceive(c *cli.Context) error {
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.IsSet("config") {
|
||||
setting.CustomConf = c.String("config")
|
||||
} else if c.GlobalIsSet("config") {
|
||||
setting.CustomConf = c.GlobalString("config")
|
||||
}
|
||||
|
||||
if err := setup("hooks/post-receive.log"); err != nil {
|
||||
fail("Hook post-receive init failed", fmt.Sprintf("setup: %v", err))
|
||||
}
|
||||
|
||||
// the environment setted on serv command
|
||||
repoUser := os.Getenv(models.EnvRepoUsername)
|
||||
repoUserSalt := os.Getenv(models.EnvRepoUserSalt)
|
||||
isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
|
||||
repoName := os.Getenv(models.EnvRepoName)
|
||||
pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
|
||||
pusherName := os.Getenv(models.EnvPusherName)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
buf.Write(scanner.Bytes())
|
||||
buf.WriteByte('\n')
|
||||
|
||||
// TODO: support news feeds for wiki
|
||||
if isWiki {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
oldCommitID := string(fields[0])
|
||||
newCommitID := string(fields[1])
|
||||
refFullName := string(fields[2])
|
||||
|
||||
if err := models.PushUpdate(models.PushUpdateOptions{
|
||||
RefFullName: refFullName,
|
||||
OldCommitID: oldCommitID,
|
||||
NewCommitID: newCommitID,
|
||||
PusherID: pusherID,
|
||||
PusherName: pusherName,
|
||||
RepoUserName: repoUser,
|
||||
RepoName: repoName,
|
||||
}); err != nil {
|
||||
log.GitLogger.Error(2, "Update: %v", err)
|
||||
}
|
||||
|
||||
// Ask for running deliver hook and test pull request tasks.
|
||||
reqURL := setting.LocalURL + repoUser + "/" + repoName + "/tasks/trigger?branch=" +
|
||||
strings.TrimPrefix(refFullName, git.BranchPrefix) + "&secret=" + base.EncodeMD5(repoUserSalt) + "&pusher=" + com.ToStr(pusherID)
|
||||
log.GitLogger.Trace("Trigger task: %s", reqURL)
|
||||
|
||||
resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).Response()
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
log.GitLogger.Error(2, "Failed to trigger task: not 2xx response code")
|
||||
}
|
||||
} else {
|
||||
log.GitLogger.Error(2, "Failed to trigger task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -14,19 +14,17 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"github.com/Unknwon/com"
|
||||
gouuid "github.com/satori/go.uuid"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
accessDenied = "Repository does not exist or you do not have access"
|
||||
accessDenied = "Repository does not exist or you do not have access"
|
||||
lfsAuthenticateVerb = "git-lfs-authenticate"
|
||||
)
|
||||
|
||||
// CmdServ represents the available serv sub-command.
|
||||
@@ -44,20 +42,20 @@ var CmdServ = cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func setup(logPath string) {
|
||||
func setup(logPath string) error {
|
||||
setting.NewContext()
|
||||
log.NewGitLogger(filepath.Join(setting.LogRootPath, logPath))
|
||||
|
||||
models.LoadConfigs()
|
||||
|
||||
if setting.UseSQLite3 || setting.UseTiDB {
|
||||
workDir, _ := setting.WorkDir()
|
||||
if err := os.Chdir(workDir); err != nil {
|
||||
log.GitLogger.Fatal(4, "Fail to change directory %s: %v", workDir, err)
|
||||
log.GitLogger.Fatal(4, "Failed to change directory %s: %v", workDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
models.SetEngine()
|
||||
setting.NewXORMLogService(true)
|
||||
return models.SetEngine()
|
||||
}
|
||||
|
||||
func parseCmd(cmd string) (string, string) {
|
||||
@@ -73,6 +71,7 @@ var (
|
||||
"git-upload-pack": models.AccessModeRead,
|
||||
"git-upload-archive": models.AccessModeRead,
|
||||
"git-receive-pack": models.AccessModeWrite,
|
||||
lfsAuthenticateVerb: models.AccessModeNone,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -91,58 +90,14 @@ func fail(userMessage, logMessage string, args ...interface{}) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func handleUpdateTask(uuid string, user, repoUser *models.User, reponame string, isWiki bool) {
|
||||
task, err := models.GetUpdateTaskByUUID(uuid)
|
||||
if err != nil {
|
||||
if models.IsErrUpdateTaskNotExist(err) {
|
||||
log.GitLogger.Trace("No update task is presented: %s", uuid)
|
||||
return
|
||||
}
|
||||
log.GitLogger.Fatal(2, "GetUpdateTaskByUUID: %v", err)
|
||||
} else if err = models.DeleteUpdateTaskByUUID(uuid); err != nil {
|
||||
log.GitLogger.Fatal(2, "DeleteUpdateTaskByUUID: %v", err)
|
||||
}
|
||||
|
||||
if isWiki {
|
||||
return
|
||||
}
|
||||
|
||||
if err = models.PushUpdate(models.PushUpdateOptions{
|
||||
RefFullName: task.RefName,
|
||||
OldCommitID: task.OldCommitID,
|
||||
NewCommitID: task.NewCommitID,
|
||||
PusherID: user.ID,
|
||||
PusherName: user.Name,
|
||||
RepoUserName: repoUser.Name,
|
||||
RepoName: reponame,
|
||||
}); err != nil {
|
||||
log.GitLogger.Error(2, "Update: %v", err)
|
||||
}
|
||||
|
||||
// Ask for running deliver hook and test pull request tasks.
|
||||
reqURL := setting.LocalURL + repoUser.Name + "/" + reponame + "/tasks/trigger?branch=" +
|
||||
strings.TrimPrefix(task.RefName, git.BranchPrefix) + "&secret=" + base.EncodeMD5(repoUser.Salt) + "&pusher=" + com.ToStr(user.ID)
|
||||
log.GitLogger.Trace("Trigger task: %s", reqURL)
|
||||
|
||||
resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}).Response()
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
log.GitLogger.Error(2, "Fail to trigger task: not 2xx response code")
|
||||
}
|
||||
} else {
|
||||
log.GitLogger.Error(2, "Fail to trigger task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runServ(c *cli.Context) error {
|
||||
if c.IsSet("config") {
|
||||
setting.CustomConf = c.String("config")
|
||||
}
|
||||
|
||||
setup("serv.log")
|
||||
if err := setup("serv.log"); err != nil {
|
||||
fail("System init failed", fmt.Sprintf("setup: %v", err))
|
||||
}
|
||||
|
||||
if setting.SSH.Disabled {
|
||||
println("Gitea: SSH has been disabled")
|
||||
@@ -161,11 +116,26 @@ func runServ(c *cli.Context) error {
|
||||
}
|
||||
|
||||
verb, args := parseCmd(cmd)
|
||||
|
||||
var lfsVerb string
|
||||
if verb == lfsAuthenticateVerb {
|
||||
if !setting.LFS.StartServer {
|
||||
fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
|
||||
}
|
||||
|
||||
argsSplit := strings.Split(args, " ")
|
||||
if len(argsSplit) >= 2 {
|
||||
args = strings.TrimSpace(argsSplit[0])
|
||||
lfsVerb = strings.TrimSpace(argsSplit[1])
|
||||
}
|
||||
}
|
||||
|
||||
repoPath := strings.ToLower(strings.Trim(args, "'"))
|
||||
rr := strings.SplitN(repoPath, "/", 2)
|
||||
if len(rr) != 2 {
|
||||
fail("Invalid repository path", "Invalid repository path: %v", args)
|
||||
}
|
||||
|
||||
username := strings.ToLower(rr[0])
|
||||
reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
|
||||
|
||||
@@ -175,6 +145,14 @@ func runServ(c *cli.Context) error {
|
||||
reponame = reponame[:len(reponame)-5]
|
||||
}
|
||||
|
||||
os.Setenv(models.EnvRepoUsername, username)
|
||||
if isWiki {
|
||||
os.Setenv(models.EnvRepoIsWiki, "true")
|
||||
} else {
|
||||
os.Setenv(models.EnvRepoIsWiki, "false")
|
||||
}
|
||||
os.Setenv(models.EnvRepoName, reponame)
|
||||
|
||||
repoUser, err := models.GetUserByName(username)
|
||||
if err != nil {
|
||||
if models.IsErrUserNotExist(err) {
|
||||
@@ -183,6 +161,8 @@ func runServ(c *cli.Context) error {
|
||||
fail("Internal error", "Failed to get repository owner (%s): %v", username, err)
|
||||
}
|
||||
|
||||
os.Setenv(models.EnvRepoUserSalt, repoUser.Salt)
|
||||
|
||||
repo, err := models.GetRepositoryByName(repoUser.ID, reponame)
|
||||
if err != nil {
|
||||
if models.IsErrRepoNotExist(err) {
|
||||
@@ -196,6 +176,16 @@ func runServ(c *cli.Context) error {
|
||||
fail("Unknown git command", "Unknown git command %s", verb)
|
||||
}
|
||||
|
||||
if verb == lfsAuthenticateVerb {
|
||||
if lfsVerb == "upload" {
|
||||
requestedMode = models.AccessModeWrite
|
||||
} else if lfsVerb == "download" {
|
||||
requestedMode = models.AccessModeRead
|
||||
} else {
|
||||
fail("Unknown LFS verb", "Unkown lfs verb %s", lfsVerb)
|
||||
}
|
||||
}
|
||||
|
||||
// Prohibit push to mirror repositories.
|
||||
if requestedMode > models.AccessModeRead && repo.IsMirror {
|
||||
fail("mirror repository is read-only", "")
|
||||
@@ -244,9 +234,9 @@ func runServ(c *cli.Context) error {
|
||||
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
|
||||
}
|
||||
|
||||
mode, err := models.AccessLevel(user, repo)
|
||||
mode, err := models.AccessLevel(user.ID, repo)
|
||||
if err != nil {
|
||||
fail("Internal error", "Fail to check access: %v", err)
|
||||
fail("Internal error", "Failed to check access: %v", err)
|
||||
} else if mode < requestedMode {
|
||||
clientMessage := accessDenied
|
||||
if mode >= models.AccessModeRead {
|
||||
@@ -257,14 +247,43 @@ func runServ(c *cli.Context) error {
|
||||
user.Name, requestedMode, repoPath)
|
||||
}
|
||||
|
||||
os.Setenv("GITEA_PUSHER_NAME", user.Name)
|
||||
os.Setenv(models.EnvPusherName, user.Name)
|
||||
os.Setenv(models.EnvPusherID, fmt.Sprintf("%d", user.ID))
|
||||
}
|
||||
}
|
||||
|
||||
uuid := gouuid.NewV4().String()
|
||||
os.Setenv("GITEA_UUID", uuid)
|
||||
// Keep the old env variable name for backward compability
|
||||
os.Setenv("uuid", uuid)
|
||||
//LFS token authentication
|
||||
if verb == lfsAuthenticateVerb {
|
||||
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, repoUser.Name, repo.Name)
|
||||
|
||||
now := time.Now()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"repo": repo.ID,
|
||||
"op": lfsVerb,
|
||||
"exp": now.Add(5 * time.Minute).Unix(),
|
||||
"nbf": now.Unix(),
|
||||
})
|
||||
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
||||
if err != nil {
|
||||
fail("Internal error", "Failed to sign JWT token: %v", err)
|
||||
}
|
||||
|
||||
tokenAuthentication := &models.LFSTokenResponse{
|
||||
Header: make(map[string]string),
|
||||
Href: url,
|
||||
}
|
||||
tokenAuthentication.Header["Authorization"] = fmt.Sprintf("Bearer %s", tokenString)
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
err = enc.Encode(tokenAuthentication)
|
||||
if err != nil {
|
||||
fail("Internal error", "Failed to encode LFS json response: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Special handle for Windows.
|
||||
if setting.IsWindows {
|
||||
@@ -278,6 +297,15 @@ func runServ(c *cli.Context) error {
|
||||
} else {
|
||||
gitcmd = exec.Command(verb, repoPath)
|
||||
}
|
||||
|
||||
if isWiki {
|
||||
if err = repo.InitWiki(); err != nil {
|
||||
fail("Internal error", "Failed to init wiki repo: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", repo.ID))
|
||||
|
||||
gitcmd.Dir = setting.RepoRootPath
|
||||
gitcmd.Stdout = os.Stdout
|
||||
gitcmd.Stdin = os.Stdin
|
||||
@@ -286,10 +314,6 @@ func runServ(c *cli.Context) error {
|
||||
fail("Internal error", "Failed to execute git command: %v", err)
|
||||
}
|
||||
|
||||
if requestedMode == models.AccessModeWrite {
|
||||
handleUpdateTask(uuid, user, repoUser, reponame, isWiki)
|
||||
}
|
||||
|
||||
// Update user key activity.
|
||||
if keyID > 0 {
|
||||
key, err := models.GetPublicKeyByID(keyID)
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright 2014 The Gogs 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 (
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// CmdUpdate represents the available update sub-command.
|
||||
var CmdUpdate = cli.Command{
|
||||
Name: "update",
|
||||
Usage: "This command should only be called by Git hook",
|
||||
Description: `Update get pushed info and insert into database`,
|
||||
Action: runUpdate,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config, c",
|
||||
Value: "custom/conf/app.ini",
|
||||
Usage: "Custom configuration file path",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runUpdate(c *cli.Context) error {
|
||||
if c.IsSet("config") {
|
||||
setting.CustomConf = c.String("config")
|
||||
}
|
||||
|
||||
setup("update.log")
|
||||
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
log.GitLogger.Trace("SSH_ORIGINAL_COMMAND is empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
if len(args) != 3 {
|
||||
log.GitLogger.Fatal(2, "Arguments received are not equal to three")
|
||||
} else if len(args[0]) == 0 {
|
||||
log.GitLogger.Fatal(2, "First argument 'refName' is empty, shouldn't use")
|
||||
}
|
||||
|
||||
task := models.UpdateTask{
|
||||
UUID: os.Getenv("GITEA_UUID"),
|
||||
RefName: args[0],
|
||||
OldCommitID: args[1],
|
||||
NewCommitID: args[2],
|
||||
}
|
||||
|
||||
if err := models.AddUpdateTask(&task); err != nil {
|
||||
log.GitLogger.Fatal(2, "AddUpdateTask: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
186
cmd/web.go
186
cmd/web.go
@@ -5,11 +5,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
"code.gitea.io/gitea/routers/org"
|
||||
"code.gitea.io/gitea/routers/repo"
|
||||
"code.gitea.io/gitea/routers/user"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/go-macaron/cache"
|
||||
"github.com/go-macaron/captcha"
|
||||
@@ -37,6 +39,7 @@ import (
|
||||
"github.com/go-macaron/i18n"
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/go-macaron/toolbox"
|
||||
context2 "github.com/gorilla/context"
|
||||
"github.com/urfave/cli"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
)
|
||||
@@ -59,16 +62,14 @@ and it takes care of all the other things for you`,
|
||||
Value: "custom/conf/app.ini",
|
||||
Usage: "Custom configuration file path",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pid, P",
|
||||
Value: "/var/run/gitea.pid",
|
||||
Usage: "Custom pid file path",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// VerChecker is a listing of required dependency versions.
|
||||
type VerChecker struct {
|
||||
ImportPath string
|
||||
Version func() string
|
||||
Expected string
|
||||
}
|
||||
|
||||
// newMacaron initializes Macaron instance.
|
||||
func newMacaron() *macaron.Macaron {
|
||||
m := macaron.New()
|
||||
@@ -82,6 +83,11 @@ func newMacaron() *macaron.Macaron {
|
||||
if setting.Protocol == setting.FCGI {
|
||||
m.SetURLPrefix(setting.AppSubURL)
|
||||
}
|
||||
m.Use(public.Custom(
|
||||
&public.Options{
|
||||
SkipLogging: setting.DisableRouterLog,
|
||||
},
|
||||
))
|
||||
m.Use(public.Static(
|
||||
&public.Options{
|
||||
Directory: path.Join(setting.StaticRootPath, "public"),
|
||||
@@ -93,6 +99,7 @@ func newMacaron() *macaron.Macaron {
|
||||
macaron.StaticOptions{
|
||||
Prefix: "avatars",
|
||||
SkipLogging: setting.DisableRouterLog,
|
||||
ETag: true,
|
||||
},
|
||||
))
|
||||
|
||||
@@ -102,7 +109,7 @@ func newMacaron() *macaron.Macaron {
|
||||
localeNames, err := options.Dir("locale")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(4, "Fail to list locale files: %v", err)
|
||||
log.Fatal(4, "Failed to list locale files: %v", err)
|
||||
}
|
||||
|
||||
localFiles := make(map[string][]byte)
|
||||
@@ -155,6 +162,11 @@ func runWeb(ctx *cli.Context) error {
|
||||
if ctx.IsSet("config") {
|
||||
setting.CustomConf = ctx.String("config")
|
||||
}
|
||||
|
||||
if ctx.IsSet("pid") {
|
||||
setting.CustomPID = ctx.String("pid")
|
||||
}
|
||||
|
||||
routers.GlobalInit()
|
||||
|
||||
m := newMacaron()
|
||||
@@ -166,6 +178,8 @@ func runWeb(ctx *cli.Context) error {
|
||||
|
||||
bindIgnErr := binding.BindIgnErr
|
||||
|
||||
m.Use(user.GetNotificationCount)
|
||||
|
||||
// FIXME: not all routes need go through same middlewares.
|
||||
// Especially some AJAX requests, we can reduce middleware number to improve performance.
|
||||
// Routers.
|
||||
@@ -186,10 +200,36 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Group("/user", func() {
|
||||
m.Get("/login", user.SignIn)
|
||||
m.Post("/login", bindIgnErr(auth.SignInForm{}), user.SignInPost)
|
||||
if setting.EnableOpenIDSignIn {
|
||||
m.Combo("/login/openid").
|
||||
Get(user.SignInOpenID).
|
||||
Post(bindIgnErr(auth.SignInOpenIDForm{}), user.SignInOpenIDPost)
|
||||
m.Group("/openid", func() {
|
||||
m.Combo("/connect").
|
||||
Get(user.ConnectOpenID).
|
||||
Post(bindIgnErr(auth.ConnectOpenIDForm{}), user.ConnectOpenIDPost)
|
||||
m.Combo("/register").
|
||||
Get(user.RegisterOpenID).
|
||||
Post(bindIgnErr(auth.SignUpOpenIDForm{}), user.RegisterOpenIDPost)
|
||||
})
|
||||
}
|
||||
m.Get("/sign_up", user.SignUp)
|
||||
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
|
||||
m.Get("/reset_password", user.ResetPasswd)
|
||||
m.Post("/reset_password", user.ResetPasswdPost)
|
||||
m.Group("/oauth2", func() {
|
||||
m.Get("/:provider", user.SignInOAuth)
|
||||
m.Get("/:provider/callback", user.SignInOAuthCallback)
|
||||
})
|
||||
m.Get("/link_account", user.LinkAccount)
|
||||
m.Post("/link_account_signin", bindIgnErr(auth.SignInForm{}), user.LinkAccountPostSignIn)
|
||||
m.Post("/link_account_signup", bindIgnErr(auth.RegisterForm{}), user.LinkAccountPostRegister)
|
||||
m.Group("/two_factor", func() {
|
||||
m.Get("", user.TwoFactor)
|
||||
m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost)
|
||||
m.Get("/scratch", user.TwoFactorScratch)
|
||||
m.Post("/scratch", bindIgnErr(auth.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
|
||||
})
|
||||
}, reqSignOut)
|
||||
|
||||
m.Group("/user/settings", func() {
|
||||
@@ -203,6 +243,15 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Post("/email/delete", user.DeleteEmail)
|
||||
m.Get("/password", user.SettingsPassword)
|
||||
m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
|
||||
if setting.EnableOpenIDSignIn {
|
||||
m.Group("/openid", func() {
|
||||
m.Combo("").Get(user.SettingsOpenID).
|
||||
Post(bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost)
|
||||
m.Post("/delete", user.DeleteOpenID)
|
||||
m.Post("/toggle_visibility", user.ToggleOpenIDVisibility)
|
||||
})
|
||||
}
|
||||
|
||||
m.Combo("/ssh").Get(user.SettingsSSHKeys).
|
||||
Post(bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost)
|
||||
m.Post("/ssh/delete", user.DeleteSSHKey)
|
||||
@@ -210,6 +259,14 @@ func runWeb(ctx *cli.Context) error {
|
||||
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
|
||||
m.Post("/applications/delete", user.SettingsDeleteApplication)
|
||||
m.Route("/delete", "GET,POST", user.SettingsDelete)
|
||||
m.Combo("/account_link").Get(user.SettingsAccountLinks).Post(user.SettingsDeleteAccountLink)
|
||||
m.Group("/two_factor", func() {
|
||||
m.Get("", user.SettingsTwoFactor)
|
||||
m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch)
|
||||
m.Post("/disable", user.SettingsTwoFactorDisable)
|
||||
m.Get("/enroll", user.SettingsTwoFactorEnroll)
|
||||
m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), user.SettingsTwoFactorEnrollPost)
|
||||
})
|
||||
}, reqSignIn, func(ctx *context.Context) {
|
||||
ctx.Data["PageIsUserSettings"] = true
|
||||
})
|
||||
@@ -219,8 +276,8 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Any("/activate", user.Activate)
|
||||
m.Any("/activate_email", user.ActivateEmail)
|
||||
m.Get("/email2user", user.Email2User)
|
||||
m.Get("/forget_password", user.ForgotPasswd)
|
||||
m.Post("/forget_password", user.ForgotPasswdPost)
|
||||
m.Get("/forgot_password", user.ForgotPasswd)
|
||||
m.Post("/forgot_password", user.ForgotPasswdPost)
|
||||
m.Get("/logout", user.SignOut)
|
||||
})
|
||||
// ***** END: User *****
|
||||
@@ -271,7 +328,6 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Get("", user.Profile)
|
||||
m.Get("/followers", user.Followers)
|
||||
m.Get("/following", user.Following)
|
||||
m.Get("/stars", user.Stars)
|
||||
})
|
||||
|
||||
m.Get("/attachments/:uuid", func(ctx *context.Context) {
|
||||
@@ -297,7 +353,7 @@ func runWeb(ctx *cli.Context) error {
|
||||
return
|
||||
}
|
||||
})
|
||||
m.Post("/issues/attachments", repo.UploadIssueAttachment)
|
||||
m.Post("/attachments", repo.UploadAttachment)
|
||||
}, ignSignIn)
|
||||
|
||||
m.Group("/:username", func() {
|
||||
@@ -313,8 +369,14 @@ func runWeb(ctx *cli.Context) error {
|
||||
|
||||
// ***** START: Organization *****
|
||||
m.Group("/org", func() {
|
||||
m.Get("/create", org.Create)
|
||||
m.Post("/create", bindIgnErr(auth.CreateOrgForm{}), org.CreatePost)
|
||||
m.Group("", func() {
|
||||
m.Get("/create", org.Create)
|
||||
m.Post("/create", bindIgnErr(auth.CreateOrgForm{}), org.CreatePost)
|
||||
}, func(ctx *context.Context) {
|
||||
if !ctx.User.CanCreateOrganization() {
|
||||
ctx.NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
m.Group("/:org", func() {
|
||||
m.Get("/dashboard", user.Dashboard)
|
||||
@@ -383,6 +445,11 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
|
||||
m.Post("/delete", repo.DeleteCollaboration)
|
||||
})
|
||||
m.Group("/branches", func() {
|
||||
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
|
||||
m.Post("/can_push", repo.ChangeProtectedBranch)
|
||||
m.Post("/delete", repo.DeleteProtectedBranch)
|
||||
}, repo.MustBeNotBare)
|
||||
|
||||
m.Group("/hooks", func() {
|
||||
m.Get("", repo.Webhooks)
|
||||
@@ -410,7 +477,7 @@ func runWeb(ctx *cli.Context) error {
|
||||
|
||||
}, func(ctx *context.Context) {
|
||||
ctx.Data["PageIsSettings"] = true
|
||||
})
|
||||
}, context.UnitTypes())
|
||||
}, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.RepoRef())
|
||||
|
||||
m.Get("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), repo.Action)
|
||||
@@ -421,17 +488,17 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue).
|
||||
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost)
|
||||
|
||||
m.Group("/:index", func() {
|
||||
m.Post("/label", repo.UpdateIssueLabel)
|
||||
m.Post("/milestone", repo.UpdateIssueMilestone)
|
||||
m.Post("/assignee", repo.UpdateIssueAssignee)
|
||||
}, reqRepoWriter)
|
||||
|
||||
m.Group("/:index", func() {
|
||||
m.Post("/title", repo.UpdateIssueTitle)
|
||||
m.Post("/content", repo.UpdateIssueContent)
|
||||
m.Post("/watch", repo.IssueWatch)
|
||||
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
|
||||
})
|
||||
|
||||
m.Post("/labels", repo.UpdateIssueLabel, reqRepoWriter)
|
||||
m.Post("/milestone", repo.UpdateIssueMilestone, reqRepoWriter)
|
||||
m.Post("/assignee", repo.UpdateIssueAssignee, reqRepoWriter)
|
||||
m.Post("/status", repo.UpdateIssueStatus, reqRepoWriter)
|
||||
})
|
||||
m.Group("/comments/:id", func() {
|
||||
m.Post("", repo.UpdateCommentContent)
|
||||
@@ -451,17 +518,15 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Get("/:id/:action", repo.ChangeMilestonStatus)
|
||||
m.Post("/delete", repo.DeleteMilestone)
|
||||
}, reqRepoWriter, context.RepoRef())
|
||||
|
||||
m.Group("/releases", func() {
|
||||
m.Get("/new", repo.NewRelease)
|
||||
m.Post("/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost)
|
||||
m.Post("/delete", repo.DeleteRelease)
|
||||
}, reqRepoWriter, context.RepoRef())
|
||||
|
||||
}, repo.MustBeNotBare, reqRepoWriter, context.RepoRef())
|
||||
m.Group("/releases", func() {
|
||||
m.Get("/edit/*", repo.EditRelease)
|
||||
m.Post("/edit/*", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)
|
||||
}, reqRepoWriter, func(ctx *context.Context) {
|
||||
}, repo.MustBeNotBare, reqRepoWriter, func(ctx *context.Context) {
|
||||
var err error
|
||||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||
if err != nil {
|
||||
@@ -500,17 +565,17 @@ func runWeb(ctx *cli.Context) error {
|
||||
return
|
||||
}
|
||||
})
|
||||
}, reqRepoWriter, context.RepoRef(), func(ctx *context.Context) {
|
||||
}, repo.MustBeNotBare, reqRepoWriter, context.RepoRef(), func(ctx *context.Context) {
|
||||
if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
|
||||
ctx.Handle(404, "", nil)
|
||||
return
|
||||
}
|
||||
})
|
||||
}, reqSignIn, context.RepoAssignment(), repo.MustBeNotBare)
|
||||
}, reqSignIn, context.RepoAssignment(), context.UnitTypes())
|
||||
|
||||
m.Group("/:username/:reponame", func() {
|
||||
m.Group("", func() {
|
||||
m.Get("/releases", repo.Releases)
|
||||
m.Get("/releases", repo.MustBeNotBare, repo.Releases)
|
||||
m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues)
|
||||
m.Get("/^:type(issues|pulls)$/:index", repo.ViewIssue)
|
||||
m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
|
||||
@@ -518,6 +583,7 @@ func runWeb(ctx *cli.Context) error {
|
||||
}, context.RepoRef())
|
||||
|
||||
// m.Get("/branches", repo.Branches)
|
||||
m.Post("/branches/:name/delete", reqSignIn, reqRepoWriter, repo.MustBeNotBare, repo.DeleteBranchPost)
|
||||
|
||||
m.Group("/wiki", func() {
|
||||
m.Get("/?:page", repo.Wiki)
|
||||
@@ -532,7 +598,12 @@ func runWeb(ctx *cli.Context) error {
|
||||
}, reqSignIn, reqRepoWriter)
|
||||
}, repo.MustEnableWiki, context.RepoRef())
|
||||
|
||||
m.Get("/archive/*", repo.Download)
|
||||
m.Group("/wiki", func() {
|
||||
m.Get("/raw/*", repo.WikiRaw)
|
||||
m.Get("/*", repo.WikiRaw)
|
||||
}, repo.MustEnableWiki)
|
||||
|
||||
m.Get("/archive/*", repo.MustBeNotBare, repo.Download)
|
||||
|
||||
m.Group("/pulls/:index", func() {
|
||||
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
|
||||
@@ -544,31 +615,46 @@ func runWeb(ctx *cli.Context) error {
|
||||
m.Get("/src/*", repo.SetEditorconfigIfExists, repo.Home)
|
||||
m.Get("/raw/*", repo.SingleDownload)
|
||||
m.Get("/commits/*", repo.RefCommits)
|
||||
m.Get("/graph", repo.Graph)
|
||||
m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.Diff)
|
||||
m.Get("/forks", repo.Forks)
|
||||
}, context.RepoRef())
|
||||
m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", repo.RawDiff)
|
||||
m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", repo.MustBeNotBare, repo.RawDiff)
|
||||
|
||||
m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.CompareDiff)
|
||||
}, ignSignIn, context.RepoAssignment(), repo.MustBeNotBare)
|
||||
m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.MustBeNotBare, repo.CompareDiff)
|
||||
}, ignSignIn, context.RepoAssignment(), context.UnitTypes())
|
||||
m.Group("/:username/:reponame", func() {
|
||||
m.Get("/stars", repo.Stars)
|
||||
m.Get("/watchers", repo.Watchers)
|
||||
}, ignSignIn, context.RepoAssignment(), context.RepoRef())
|
||||
}, ignSignIn, context.RepoAssignment(), context.RepoRef(), context.UnitTypes())
|
||||
|
||||
m.Group("/:username", func() {
|
||||
m.Group("/:reponame", func() {
|
||||
m.Get("", repo.SetEditorconfigIfExists, repo.Home)
|
||||
m.Get("\\.git$", repo.SetEditorconfigIfExists, repo.Home)
|
||||
}, ignSignIn, context.RepoAssignment(true), context.RepoRef())
|
||||
}, ignSignIn, context.RepoAssignment(), context.RepoRef(), context.UnitTypes())
|
||||
|
||||
m.Group("/:reponame", func() {
|
||||
m.Group("/info/lfs", func() {
|
||||
m.Post("/objects/batch", lfs.BatchHandler)
|
||||
m.Get("/objects/:oid/:filename", lfs.ObjectOidHandler)
|
||||
m.Any("/objects/:oid", lfs.ObjectOidHandler)
|
||||
m.Post("/objects", lfs.PostHandler)
|
||||
m.Any("/*", func(ctx *context.Context) {
|
||||
ctx.Handle(404, "", nil)
|
||||
})
|
||||
}, ignSignInAndCsrf)
|
||||
m.Any("/*", ignSignInAndCsrf, repo.HTTP)
|
||||
m.Head("/tasks/trigger", repo.TriggerTask)
|
||||
})
|
||||
})
|
||||
// ***** END: Repository *****
|
||||
|
||||
m.Group("/notifications", func() {
|
||||
m.Get("", user.Notifications)
|
||||
m.Post("/status", user.NotificationStatusPost)
|
||||
}, reqSignIn)
|
||||
|
||||
m.Group("/api", func() {
|
||||
apiv1.RegisterRoutes(m)
|
||||
}, ignSignIn)
|
||||
@@ -599,18 +685,32 @@ func runWeb(ctx *cli.Context) error {
|
||||
}
|
||||
log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL)
|
||||
|
||||
if setting.LFS.StartServer {
|
||||
log.Info("LFS server enabled")
|
||||
}
|
||||
|
||||
if setting.EnablePprof {
|
||||
go func() {
|
||||
log.Info("%v", http.ListenAndServe("localhost:6060", nil))
|
||||
}()
|
||||
}
|
||||
|
||||
var err error
|
||||
switch setting.Protocol {
|
||||
case setting.HTTP:
|
||||
err = http.ListenAndServe(listenAddr, m)
|
||||
err = runHTTP(listenAddr, context2.ClearHandler(m))
|
||||
case setting.HTTPS:
|
||||
server := &http.Server{Addr: listenAddr, TLSConfig: &tls.Config{MinVersion: tls.VersionTLS10}, Handler: m}
|
||||
err = server.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||
err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m))
|
||||
case setting.FCGI:
|
||||
err = fcgi.Serve(nil, m)
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
log.Fatal(4, "Failed to bind %s", listenAddr, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
err = fcgi.Serve(listener, context2.ClearHandler(m))
|
||||
case setting.UnixSocket:
|
||||
if err := os.Remove(listenAddr); err != nil {
|
||||
log.Fatal(4, "Fail to remove unix socket directory %s: %v", listenAddr, err)
|
||||
if err := os.Remove(listenAddr); err != nil && !os.IsNotExist(err) {
|
||||
log.Fatal(4, "Failed to remove unix socket directory %s: %v", listenAddr, err)
|
||||
}
|
||||
var listener *net.UnixListener
|
||||
listener, err = net.ListenUnix("unix", &net.UnixAddr{Name: listenAddr, Net: "unix"})
|
||||
@@ -623,13 +723,13 @@ func runWeb(ctx *cli.Context) error {
|
||||
if err = os.Chmod(listenAddr, os.FileMode(setting.UnixSocketPermission)); err != nil {
|
||||
log.Fatal(4, "Failed to set permission of unix socket: %v", err)
|
||||
}
|
||||
err = http.Serve(listener, m)
|
||||
err = http.Serve(listener, context2.ClearHandler(m))
|
||||
default:
|
||||
log.Fatal(4, "Invalid protocol: %s", setting.Protocol)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(4, "Fail to start server: %v", err)
|
||||
log.Fatal(4, "Failed to start server: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
44
cmd/web_graceful.go
Normal file
44
cmd/web_graceful.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// +build !windows
|
||||
|
||||
// Copyright 2016 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"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"github.com/facebookgo/grace/gracehttp"
|
||||
)
|
||||
|
||||
func runHTTP(listenAddr string, m http.Handler) error {
|
||||
return gracehttp.Serve(&http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: m,
|
||||
})
|
||||
}
|
||||
|
||||
func runHTTPS(listenAddr, certFile, keyFile string, m http.Handler) error {
|
||||
config := &tls.Config{
|
||||
MinVersion: tls.VersionTLS10,
|
||||
}
|
||||
if config.NextProtos == nil {
|
||||
config.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
config.Certificates = make([]tls.Certificate, 1)
|
||||
var err error
|
||||
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatal(4, "Failed to load https cert file %s: %v", listenAddr, err)
|
||||
}
|
||||
|
||||
return gracehttp.Serve(&http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: m,
|
||||
TLSConfig: config,
|
||||
})
|
||||
}
|
||||
19
cmd/web_windows.go
Normal file
19
cmd/web_windows.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// +build windows
|
||||
|
||||
// Copyright 2016 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 (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func runHTTP(listenAddr string, m http.Handler) error {
|
||||
return http.ListenAndServe(listenAddr, m)
|
||||
}
|
||||
|
||||
func runHTTPS(listenAddr, certFile, keyFile string, m http.Handler) error {
|
||||
return http.ListenAndServeTLS(listenAddr, certFile, keyFile, m)
|
||||
}
|
||||
78
conf/app.ini
vendored
78
conf/app.ini
vendored
@@ -57,6 +57,8 @@ FEED_MAX_COMMIT_NUM = 5
|
||||
THEME_COLOR_META_TAG = `#6cc644`
|
||||
; Max size of files to be displayed (defaults is 8MiB)
|
||||
MAX_DISPLAY_FILE_SIZE = 8388608
|
||||
; Whether show the user email in the Explore Users page
|
||||
SHOW_USER_EMAIL = true
|
||||
|
||||
[ui.admin]
|
||||
; Number of users that are showed in one page
|
||||
@@ -101,7 +103,7 @@ START_SSH_SERVER = false
|
||||
; Domain name to be exposed in clone URL
|
||||
SSH_DOMAIN = %(DOMAIN)s
|
||||
; Network interface builtin SSH server listens on
|
||||
SSH_LISTEN_HOST =
|
||||
SSH_LISTEN_HOST =
|
||||
; Port number to be exposed in clone URL
|
||||
SSH_PORT = 22
|
||||
; Port number builtin SSH server listens on
|
||||
@@ -145,7 +147,7 @@ RSA = 2048
|
||||
DSA = 1024
|
||||
|
||||
[database]
|
||||
; Either "mysql", "postgres" or "sqlite3", it's your choice
|
||||
; Either "mysql", "postgres", "mssql" or "sqlite3", it's your choice
|
||||
DB_TYPE = mysql
|
||||
HOST = 127.0.0.1:3306
|
||||
NAME = gitea
|
||||
@@ -156,9 +158,16 @@ SSL_MODE = disable
|
||||
; For "sqlite3" and "tidb", use absolute path when you start as service
|
||||
PATH = data/gitea.db
|
||||
|
||||
[indexer]
|
||||
ISSUE_INDEXER_PATH = indexers/issues.bleve
|
||||
UPDATE_BUFFER_LEN = 20
|
||||
|
||||
[admin]
|
||||
; Disable regular (non-admin) users to create organizations
|
||||
DISABLE_REGULAR_ORG_CREATION = false
|
||||
|
||||
[security]
|
||||
; Whether the installer is disabled
|
||||
INSTALL_LOCK = false
|
||||
; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!!
|
||||
SECRET_KEY = !#@FDEWREWR&*(
|
||||
@@ -168,6 +177,43 @@ COOKIE_USERNAME = gitea_awesome
|
||||
COOKIE_REMEMBER_NAME = gitea_incredible
|
||||
; Reverse proxy authentication header name of user name
|
||||
REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
|
||||
; Sets the minimum password length for new Users
|
||||
MIN_PASSWORD_LENGTH = 6
|
||||
; True when users are allowed to import local server paths
|
||||
IMPORT_LOCAL_PATHS = false
|
||||
|
||||
[openid]
|
||||
;
|
||||
; OpenID is an open standard and decentralized authentication protocol.
|
||||
; Your identity is the address of a webpage you provide, which describes
|
||||
; how to prove you are in control of that page.
|
||||
;
|
||||
; For more info: https://en.wikipedia.org/wiki/OpenID
|
||||
;
|
||||
; Current implementation supports OpenID-2.0
|
||||
;
|
||||
; Tested to work providers at the time of writing:
|
||||
; - Any GNUSocial node (your.hostname.tld/username)
|
||||
; - Any SimpleID provider (http://simpleid.koinic.net)
|
||||
; - http://openid.org.cn/
|
||||
; - openid.stackexchange.com
|
||||
; - login.launchpad.net
|
||||
; - <username>.livejournal.com
|
||||
;
|
||||
; Whether to allow signin in via OpenID
|
||||
ENABLE_OPENID_SIGNIN = true
|
||||
; Whether to allow registering via OpenID
|
||||
ENABLE_OPENID_SIGNUP = true
|
||||
; Allowed URI patterns (POSIX regexp).
|
||||
; Space separated.
|
||||
; Only these would be allowed if non-blank.
|
||||
; Example value: trusted.domain.org trusted.domain.net
|
||||
WHITELISTED_URIS =
|
||||
; Forbidden URI patterns (POSIX regexp).
|
||||
; Space sepaated.
|
||||
; Only used if WHITELISTED_URIS is blank.
|
||||
; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
|
||||
BLACKLISTED_URIS =
|
||||
|
||||
[service]
|
||||
ACTIVE_CODE_LIVE_MINUTES = 180
|
||||
@@ -185,6 +231,13 @@ ENABLE_REVERSE_PROXY_AUTHENTICATION = false
|
||||
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
|
||||
; Enable captcha validation for registration
|
||||
ENABLE_CAPTCHA = true
|
||||
; Default value for KeepEmailPrivate
|
||||
; New user will get the value of this setting copied into their profile
|
||||
DEFAULT_KEEP_EMAIL_PRIVATE = false
|
||||
; Default value for the domain part of the user's email address in the git log
|
||||
; if he has set KeepEmailPrivate true. The user's email replaced with a
|
||||
; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS.
|
||||
NO_REPLY_ADDRESS = noreply.example.org
|
||||
|
||||
[webhook]
|
||||
; Hook task queue length, increase if webhook shooting starts hanging
|
||||
@@ -224,6 +277,10 @@ USER =
|
||||
PASSWD =
|
||||
; Use text/html as alternative format of content
|
||||
ENABLE_HTML_ALTERNATIVE = false
|
||||
; Enable sendmail (override SMTP)
|
||||
USE_SENDMAIL = false
|
||||
; Specifiy an alternative sendmail binary
|
||||
SENDMAIL_PATH = sendmail
|
||||
|
||||
[cache]
|
||||
; Either "memory", "redis", or "memcache", default is "memory"
|
||||
@@ -245,14 +302,14 @@ PROVIDER = memory
|
||||
; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
||||
PROVIDER_CONFIG = data/sessions
|
||||
; Session cookie name
|
||||
COOKIE_NAME = i_like_gogits
|
||||
COOKIE_NAME = i_like_gitea
|
||||
; If you use session in https only, default is false
|
||||
COOKIE_SECURE = false
|
||||
; Enable set cookie, default is true
|
||||
ENABLE_SET_COOKIE = true
|
||||
; Session GC time interval, default is 86400
|
||||
; Session GC time interval in seconds, default is 86400 (1 day)
|
||||
GC_INTERVAL_TIME = 86400
|
||||
; Session life time, default is 86400
|
||||
; Session life time in seconds, default is 86400 (1 day)
|
||||
SESSION_LIFE_TIME = 86400
|
||||
|
||||
[picture]
|
||||
@@ -273,7 +330,7 @@ ENABLE = true
|
||||
; Path for attachments. Defaults to `data/attachments`
|
||||
PATH = data/attachments
|
||||
; One or more allowed types, e.g. image/jpeg|image/png
|
||||
ALLOWED_TYPES = image/jpeg|image/png
|
||||
ALLOWED_TYPES = image/jpeg|image/png|application/zip|application/gzip
|
||||
; Max size of each file. Defaults to 32MB
|
||||
MAX_SIZE = 4
|
||||
; Max number of files per upload. Defaults to 10
|
||||
@@ -369,6 +426,13 @@ ARGS =
|
||||
RUN_AT_START = true
|
||||
SCHEDULE = @every 24h
|
||||
|
||||
; Clean up old repository archives
|
||||
[cron.archive_cleanup]
|
||||
RUN_AT_START = true
|
||||
SCHEDULE = @every 24h
|
||||
; Archives created more than OLDER_THAN ago are subject to deletion
|
||||
OLDER_THAN = 24h
|
||||
|
||||
[git]
|
||||
; Disables highlight of added and removed changes
|
||||
DISABLE_DIFF_HIGHLIGHT = false
|
||||
@@ -400,7 +464,7 @@ MAX_RESPONSE_ITEMS = 50
|
||||
|
||||
[i18n]
|
||||
LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR
|
||||
NAMES = English,简体中文,繁體中文(香港),繁體中文(台湾),Deutsch,Français,Nederlands,Latviešu,Русский,日本語,Español,Português do Brasil,Polski,български,Italiano,Suomalainen,Türkçe,čeština,Српски,Svenska,한국어
|
||||
NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,Français,Nederlands,Latviešu,Русский,日本語,Español,Português do Brasil,Polski,български,Italiano,Suomalainen,Türkçe,čeština,Српски,Svenska,한국어
|
||||
|
||||
; Used for datetimepicker
|
||||
[i18n.datelang]
|
||||
|
||||
86
contrib/init/debian/gitea
Normal file
86
contrib/init/debian/gitea
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: gitea
|
||||
# Required-Start: $syslog $network
|
||||
# Required-Stop: $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: A self-hosted Git service written in Go.
|
||||
# Description: A self-hosted Git service written in Go.
|
||||
### END INIT INFO
|
||||
|
||||
# Author: Danny Boisvert
|
||||
|
||||
# Do NOT "set -e"
|
||||
|
||||
# PATH should only include /usr/* if it runs after the mountnfs.sh script
|
||||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
DESC="Go Git Service"
|
||||
NAME=gitea
|
||||
SERVICEVERBOSE=yes
|
||||
PIDFILE=/var/run/$NAME.pid
|
||||
SCRIPTNAME=/etc/init.d/$NAME
|
||||
WORKINGDIR=/home/git/gitea
|
||||
DAEMON=$WORKINGDIR/$NAME
|
||||
DAEMON_ARGS="web"
|
||||
USER=git
|
||||
USERBIND="setcap cap_net_bind_service=+ep"
|
||||
STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/1/KILL/5}"
|
||||
|
||||
# Read configuration variable file if it is present
|
||||
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
|
||||
|
||||
# Exit if the package is not installed
|
||||
[ -x "$DAEMON" ] || exit 0
|
||||
|
||||
do_start()
|
||||
{
|
||||
$USERBIND $DAEMON
|
||||
sh -c "USER=$USER start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\
|
||||
--background --chdir $WORKINGDIR --chuid $USER \\
|
||||
--exec $DAEMON -- $DAEMON_ARGS"
|
||||
}
|
||||
|
||||
do_stop()
|
||||
{
|
||||
start-stop-daemon --stop --quiet --retry=$STOP_SCHEDULE --pidfile $PIDFILE --name $NAME --oknodo
|
||||
rm -f $PIDFILE
|
||||
}
|
||||
|
||||
do_status()
|
||||
{
|
||||
if [ -f $PIDFILE ]; then
|
||||
if kill -0 $(cat "$PIDFILE"); then
|
||||
echo "$NAME is running, PID is $(cat $PIDFILE)"
|
||||
else
|
||||
echo "$NAME process is dead, but pidfile exists"
|
||||
fi
|
||||
else
|
||||
echo "$NAME is not running"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "Starting $DESC" "$NAME"
|
||||
do_start
|
||||
;;
|
||||
stop)
|
||||
echo "Stopping $DESC" "$NAME"
|
||||
do_stop
|
||||
;;
|
||||
status)
|
||||
do_status
|
||||
;;
|
||||
restart)
|
||||
echo "Restarting $DESC" "$NAME"
|
||||
do_stop
|
||||
do_start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $SCRIPTNAME {start|stop|status|restart}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
206
contrib/migrate/gogs_migrate.sh
Executable file
206
contrib/migrate/gogs_migrate.sh
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
|
||||
gitea_version=1.0.1
|
||||
tested_gogs_version="0.9.114.1227"
|
||||
gogs_binary=gogs
|
||||
gitea_binary=gitea
|
||||
download_gitea=true
|
||||
gitea_path=
|
||||
|
||||
function usage() {
|
||||
echo "Optional parameters: [-b Gitea binary] [-i Gitea install dir] [-o gogs binary] [-h help]";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
while getopts ":b::i:o:h:" opt; do
|
||||
case $opt in
|
||||
b)
|
||||
gitea_binary=${OPTARG}
|
||||
download_gitea=false
|
||||
;;
|
||||
i)
|
||||
gitea_path=${OPTARG}
|
||||
;;
|
||||
o)
|
||||
gogs_binary=${OPTARG}
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
;;
|
||||
\?)
|
||||
echo -e "Invalid option: -$OPTARG"
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
function exitOnError() {
|
||||
if [ "$?" != "0" ]; then
|
||||
echo -e $1
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function checkBinary() {
|
||||
if [ ! -f $1 ]; then
|
||||
echo "Unable to find $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function continueYN(){
|
||||
while true; do
|
||||
echo -e "$1 Yes or No"
|
||||
read yn
|
||||
case $yn in
|
||||
[Yy]* ) break;;
|
||||
[Nn]* ) exit 1;;
|
||||
* ) echo "Please answer yes or no.";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
########## Binary checks
|
||||
if pidof "$gogs_binary" >/dev/null; then
|
||||
echo "Please stop gogs before migrating to Gitea"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checkBinary "$gogs_binary"
|
||||
|
||||
if [ ! -x "$gogs_binary" ]; then
|
||||
echo "Please make sure that you are running this script as the gogs user"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
########## Version check
|
||||
gogs_version=$(./$gogs_binary --version)
|
||||
original_IFS=$IFS
|
||||
IFS="." && current_version=(${gogs_version#"Gogs version "}) && minimal_version=($tested_gogs_version)
|
||||
IFS=$original_IFS
|
||||
|
||||
count=0
|
||||
for i in "${current_version[@]}"
|
||||
do
|
||||
if [ $i -gt ${minimal_version[$count]} ]; then
|
||||
echo -e "!!!--WARNING--!!!\nYour $gogs_version is newer than the tested Gogs version $tested_gogs_version\nUse this script on your own risk\n!!!--WARNING--!!!"
|
||||
break
|
||||
fi
|
||||
let count+=1
|
||||
done
|
||||
|
||||
########## Disclaimer
|
||||
continueYN "This migration script creates a backup before it starts with the actual migration
|
||||
If something goes wrong you could always resotre this backup.
|
||||
The backups are stored into your gogs folder in gogs-dump-[timestamp].zip file
|
||||
|
||||
Migrating from gogs to gitea, are you sure?"
|
||||
|
||||
########## gogs dump
|
||||
echo "Creating a backup of gogs, this could take a while..."
|
||||
./"$gogs_binary" dump
|
||||
exitOnError "Failed to create a gogs dump"
|
||||
|
||||
########## Create Gitea folder
|
||||
if [ -z "$gitea_path" ]; then
|
||||
echo "Where do you want to install Gitea?"
|
||||
read gitea_path
|
||||
fi
|
||||
|
||||
if [ ! -d "$gitea_path" ]; then
|
||||
mkdir -p "$gitea_path"
|
||||
exitOnError
|
||||
fi
|
||||
|
||||
if [ "$(ls -A $gitea_path)" ]; then
|
||||
continueYN "!!!--WARNING--!!!\nDirectory $gitea_path is not empty, do you want to continue?"
|
||||
fi
|
||||
|
||||
|
||||
########## Download Gitea
|
||||
if [ $download_gitea == true ]; then
|
||||
|
||||
########## Detect os
|
||||
case "$OSTYPE" in
|
||||
darwin*) platform="darwin-10.6";;
|
||||
linux*) platform="linux" ;;
|
||||
freebsd*) platform="bsd" ;;
|
||||
netbsd*) platform="bsd" ;;
|
||||
openbsd*) platform="bsd" ;;
|
||||
*) echo "Unsupported os: $OSTYPE\n Please download/compile your own binary and run this script with the -b option" exit 1;;
|
||||
esac
|
||||
|
||||
arch=""
|
||||
bits=""
|
||||
if [[ "$platform" == "linux" ]] || [[ "$platform" == "bsd" ]]; then
|
||||
arch="$(uname -m | sed -e 's/arm\(.*\)/arm-\1/' -e s/aarch64.*/arm64/)"
|
||||
fi
|
||||
|
||||
if [[ "$platform" == "bsd" ]] && [[ "$arch" != "arm"* ]]; then
|
||||
echo "Currently Gitea only supports arm prebuilt binarys on bsd"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$arch" != "arm"* ]] && [[ "$arch" != "mips"* ]]; then
|
||||
arch=""
|
||||
case "$(getconf LONG_BIT)" in
|
||||
64*) bits="amd64";;
|
||||
32*) bits="386" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
########## Wget Gitea
|
||||
echo "Downloading Gitea"
|
||||
file="gitea-$gitea_version-$platform-$arch$bits"
|
||||
url="https://dl.gitea.io/gitea/$gitea_version/$file"
|
||||
wget "$url" -P "$gitea_path"
|
||||
exitOnError "Failed to download $url"
|
||||
|
||||
wget "$url.sha256" -P "$gitea_path"
|
||||
exitOnError "Failed to Gitea checksum $url.sha256"
|
||||
|
||||
echo "Comparing checksums"
|
||||
gogs_dir=$(pwd)
|
||||
cd "$gitea_path"
|
||||
|
||||
sha256sum -c "$file.sha256"
|
||||
exitOnError "Downloaded Gitea checksums do not match"
|
||||
|
||||
rm "$file.sha256"
|
||||
mv "$file" gitea
|
||||
cd "$gogs_dir"
|
||||
|
||||
else
|
||||
checkBinary "$gitea_binary"
|
||||
if [ "$gitea_binary" != "$gitea_path/gitea" ];then
|
||||
cp "$gitea_binary" "$gitea_path/gitea"
|
||||
fi
|
||||
fi
|
||||
|
||||
########## Copy gogs data to Gitea folder
|
||||
echo "Copying gogs data to Gitea, this could take a while..."
|
||||
cp -R custom "$gitea_path"
|
||||
cp -R data "$gitea_path"
|
||||
#cp -R conf "$gitea_path"
|
||||
|
||||
########## Moving & deleting old files
|
||||
#mv $gitea_path/conf $gitea_path/options
|
||||
cd "$gitea_path"
|
||||
mv "custom/conf/app.ini" "custom/conf/gogs_app.ini"
|
||||
url="https://raw.githubusercontent.com/go-gitea/gitea/v$gitea_version/conf/app.ini"
|
||||
wget "$url" -P "custom/conf/"
|
||||
exitOnError "Unable to download Gitea app.ini"
|
||||
rm -f conf/README.md
|
||||
|
||||
echo -e "Migration is almost complete, you only need to merge custom/conf/gogs_app.ini into custom/conf/app.ini"
|
||||
continueYN "Do you want to start Gitea?"
|
||||
|
||||
########## Starting Gitea
|
||||
echo "Starting Gitea"
|
||||
chmod +x gitea
|
||||
./gitea web
|
||||
exitOnError "Failed to start Gitea"
|
||||
2
contrib/mysql.sql
Normal file
2
contrib/mysql.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DROP DATABASE IF EXISTS gitea;
|
||||
CREATE DATABASE IF NOT EXISTS gitea CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
91
integrations/install_test.go
Normal file
91
integrations/install_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright 2017 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 integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/integrations/internal/utils"
|
||||
)
|
||||
|
||||
// The HTTP port listened by the Gitea server.
|
||||
const ServerHTTPPort = "3001"
|
||||
|
||||
const _RetryLimit = 10
|
||||
|
||||
func makeSimpleSettings(user, port string) map[string][]string {
|
||||
return map[string][]string{
|
||||
"db_type": {"SQLite3"},
|
||||
"db_host": {"localhost"},
|
||||
"db_path": {"data/gitea.db"},
|
||||
"app_name": {"Gitea: Git with a cup of tea"},
|
||||
"repo_root_path": {"repositories"},
|
||||
"run_user": {user},
|
||||
"domain": {"localhost"},
|
||||
"ssh_port": {"22"},
|
||||
"http_port": {port},
|
||||
"app_url": {"http://localhost:" + port},
|
||||
"log_root_path": {"log"},
|
||||
}
|
||||
}
|
||||
|
||||
func install(t *utils.T) error {
|
||||
var r *http.Response
|
||||
var err error
|
||||
|
||||
for i := 1; i <= _RetryLimit; i++ {
|
||||
|
||||
r, err = http.Get("http://:" + ServerHTTPPort + "/")
|
||||
if err == nil {
|
||||
fmt.Fprintln(os.Stderr)
|
||||
break
|
||||
}
|
||||
|
||||
// Give the server some amount of time to warm up.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
fmt.Fprint(os.Stderr, ".")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
_user, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings := makeSimpleSettings(_user.Username, ServerHTTPPort)
|
||||
r, err = http.PostForm("http://:"+ServerHTTPPort+"/install", settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("'/install': %s", r.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
conf := utils.Config{
|
||||
Program: "../gitea",
|
||||
WorkDir: "",
|
||||
Args: []string{"web", "--port", ServerHTTPPort},
|
||||
LogFile: os.Stderr,
|
||||
}
|
||||
|
||||
if err := utils.New(t, &conf).RunTest(install); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
156
integrations/internal/utils/utils.go
Normal file
156
integrations/internal/utils/utils.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2017 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 utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// T wraps testing.T and the configurations of the testing instance.
|
||||
type T struct {
|
||||
*testing.T
|
||||
Config *Config
|
||||
}
|
||||
|
||||
// New create an instance of T
|
||||
func New(t *testing.T, c *Config) *T {
|
||||
return &T{T: t, Config: c}
|
||||
}
|
||||
|
||||
// Config Settings of the testing program
|
||||
type Config struct {
|
||||
// The executable path of the tested program.
|
||||
Program string
|
||||
// Working directory prepared for the tested program.
|
||||
// If empty, a directory named with random suffixes is picked, and created under the platform-dependent default temporary directory.
|
||||
// The directory will be removed when the test finishes.
|
||||
WorkDir string
|
||||
// Command-line arguments passed to the tested program.
|
||||
Args []string
|
||||
|
||||
// Where to redirect the stdout/stderr to. For debugging purposes.
|
||||
LogFile *os.File
|
||||
}
|
||||
|
||||
func redirect(cmd *exec.Cmd, f *os.File) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go io.Copy(f, stdout)
|
||||
go io.Copy(f, stderr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunTest Helper function for setting up a running Gitea server for functional testing and then gracefully terminating it.
|
||||
func (t *T) RunTest(tests ...func(*T) error) (err error) {
|
||||
if t.Config.Program == "" {
|
||||
return errors.New("Need input file")
|
||||
}
|
||||
|
||||
path, err := filepath.Abs(t.Config.Program)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workdir := t.Config.WorkDir
|
||||
if workdir == "" {
|
||||
workdir, err = ioutil.TempDir(os.TempDir(), "gitea_tests-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(workdir)
|
||||
}
|
||||
|
||||
newpath := filepath.Join(workdir, filepath.Base(path))
|
||||
if err := os.Symlink(path, newpath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Starting the server: %s args:%s workdir:%s", newpath, t.Config.Args, workdir)
|
||||
|
||||
cmd := exec.Command(newpath, t.Config.Args...)
|
||||
cmd.Dir = workdir
|
||||
|
||||
if t.Config.LogFile != nil && testing.Verbose() {
|
||||
if err := redirect(cmd, t.Config.LogFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Server started.")
|
||||
|
||||
defer func() {
|
||||
// Do not early return. We have to call Wait anyway.
|
||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
||||
|
||||
if _err := cmd.Wait(); _err != nil {
|
||||
if _err.Error() != "signal: terminated" {
|
||||
err = _err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Server exited")
|
||||
}()
|
||||
|
||||
for _, fn := range tests {
|
||||
if err := fn(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Note that the return value 'err' may be updated by the 'defer' statement before despite it's returning nil here.
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAndPost provides a convenient helper function for testing an HTTP endpoint with GET and POST method.
|
||||
// The function sends GET first and then POST with the given form.
|
||||
func GetAndPost(url string, form map[string][]string) error {
|
||||
var err error
|
||||
var r *http.Response
|
||||
|
||||
r, err = http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("GET '%s': %s", url, r.Status)
|
||||
}
|
||||
|
||||
r, err = http.PostForm(url, form)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("POST '%s': %s", url, r.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
integrations/signup_test.go
Normal file
35
integrations/signup_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2017 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 integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/integrations/internal/utils"
|
||||
)
|
||||
|
||||
var signupFormSample map[string][]string = map[string][]string{
|
||||
"Name": {"tester"},
|
||||
"Email": {"user1@example.com"},
|
||||
"Passwd": {"12345678"},
|
||||
}
|
||||
|
||||
func signup(t *utils.T) error {
|
||||
return utils.GetAndPost("http://:"+ServerHTTPPort+"/user/sign_up", signupFormSample)
|
||||
}
|
||||
|
||||
func TestSignup(t *testing.T) {
|
||||
conf := utils.Config{
|
||||
Program: "../gitea",
|
||||
WorkDir: "",
|
||||
Args: []string{"web", "--port", ServerHTTPPort},
|
||||
LogFile: os.Stderr,
|
||||
}
|
||||
|
||||
if err := utils.New(t, &conf).RunTest(install, signup); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
82
integrations/version_test.go
Normal file
82
integrations/version_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2017 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 integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/integrations/internal/utils"
|
||||
"code.gitea.io/sdk/gitea"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func version(t *utils.T) error {
|
||||
var err error
|
||||
|
||||
path, err := filepath.Abs(t.Config.Program)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "--version")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(out))
|
||||
if !strings.HasPrefix(string(out), "Gitea version") {
|
||||
return fmt.Errorf("unexpected version string '%s' of the gitea executable", out)
|
||||
}
|
||||
|
||||
expected := fields[2]
|
||||
|
||||
var r *http.Response
|
||||
r, err = http.Get("http://:" + ServerHTTPPort + "/api/v1/version")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("'/api/v1/version': %s\n", r.Status)
|
||||
}
|
||||
|
||||
var v gitea.ServerVersion
|
||||
|
||||
dec := json.NewDecoder(r.Body)
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actual := v.Version
|
||||
|
||||
log.Printf("Actual: \"%s\" Expected: \"%s\"\n", actual, expected)
|
||||
assert.Equal(t, expected, actual)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
conf := utils.Config{
|
||||
Program: "../gitea",
|
||||
WorkDir: "",
|
||||
Args: []string{"web", "--port", ServerHTTPPort},
|
||||
LogFile: os.Stderr,
|
||||
}
|
||||
|
||||
if err := utils.New(t, &conf).RunTest(install, version); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
25
main.go
25
main.go
@@ -8,32 +8,34 @@ package main // import "code.gitea.io/gitea"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/cmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Version holds the current Gitea version
|
||||
var Version = "1.0.0+dev"
|
||||
var Version = "1.1.0+dev"
|
||||
|
||||
// Tags holds the build tags used
|
||||
var Tags = ""
|
||||
|
||||
func init() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
setting.AppVer = Version
|
||||
setting.AppBuiltWith = formatBuiltWith(Tags)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "Gitea"
|
||||
app.Usage = "A painless self-hosted Git service"
|
||||
app.Version = Version
|
||||
app.Version = Version + formatBuiltWith(Tags)
|
||||
app.Commands = []cli.Command{
|
||||
cmd.CmdWeb,
|
||||
cmd.CmdServ,
|
||||
cmd.CmdUpdate,
|
||||
cmd.CmdHook,
|
||||
cmd.CmdDump,
|
||||
cmd.CmdCert,
|
||||
cmd.CmdAdmin,
|
||||
@@ -41,7 +43,14 @@ func main() {
|
||||
app.Flags = append(app.Flags, []cli.Flag{}...)
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(4, "Fail to run app with %s: %v", os.Args, err)
|
||||
log.Fatal(4, "Failed to run app with %s: %v", os.Args, err)
|
||||
}
|
||||
}
|
||||
|
||||
func formatBuiltWith(Tags string) string {
|
||||
if len(Tags) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return " built with: " + strings.Replace(Tags, " ", ", ", -1)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
// AccessMode specifies the users access mode
|
||||
type AccessMode int
|
||||
@@ -63,21 +59,21 @@ type Access struct {
|
||||
Mode AccessMode
|
||||
}
|
||||
|
||||
func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
|
||||
func accessLevel(e Engine, userID int64, repo *Repository) (AccessMode, error) {
|
||||
mode := AccessModeNone
|
||||
if !repo.IsPrivate {
|
||||
mode = AccessModeRead
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
if userID == 0 {
|
||||
return mode, nil
|
||||
}
|
||||
|
||||
if user.ID == repo.OwnerID {
|
||||
if userID == repo.OwnerID {
|
||||
return AccessModeOwner, nil
|
||||
}
|
||||
|
||||
a := &Access{UserID: user.ID, RepoID: repo.ID}
|
||||
a := &Access{UserID: userID, RepoID: repo.ID}
|
||||
if has, err := e.Get(a); !has || err != nil {
|
||||
return mode, err
|
||||
}
|
||||
@@ -85,44 +81,60 @@ func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
|
||||
}
|
||||
|
||||
// AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the
|
||||
// user does not have access. User can be nil!
|
||||
func AccessLevel(user *User, repo *Repository) (AccessMode, error) {
|
||||
return accessLevel(x, user, repo)
|
||||
// user does not have access.
|
||||
func AccessLevel(userID int64, repo *Repository) (AccessMode, error) {
|
||||
return accessLevel(x, userID, repo)
|
||||
}
|
||||
|
||||
func hasAccess(e Engine, user *User, repo *Repository, testMode AccessMode) (bool, error) {
|
||||
mode, err := accessLevel(e, user, repo)
|
||||
func hasAccess(e Engine, userID int64, repo *Repository, testMode AccessMode) (bool, error) {
|
||||
mode, err := accessLevel(e, userID, repo)
|
||||
return testMode <= mode, err
|
||||
}
|
||||
|
||||
// HasAccess returns true if someone has the request access level. User can be nil!
|
||||
func HasAccess(user *User, repo *Repository, testMode AccessMode) (bool, error) {
|
||||
return hasAccess(x, user, repo, testMode)
|
||||
// HasAccess returns true if user has access to repo
|
||||
func HasAccess(userID int64, repo *Repository, testMode AccessMode) (bool, error) {
|
||||
return hasAccess(x, userID, repo, testMode)
|
||||
}
|
||||
|
||||
type repoAccess struct {
|
||||
Access `xorm:"extends"`
|
||||
Repository `xorm:"extends"`
|
||||
}
|
||||
|
||||
func (repoAccess) TableName() string {
|
||||
return "access"
|
||||
}
|
||||
|
||||
// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own.
|
||||
func (user *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) {
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err := x.Find(&accesses, &Access{UserID: user.ID}); err != nil {
|
||||
rows, err := x.
|
||||
Join("INNER", "repository", "repository.id = access.repo_id").
|
||||
Where("access.user_id = ?", user.ID).
|
||||
And("repository.owner_id <> ?", user.ID).
|
||||
Rows(new(repoAccess))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
repos := make(map[*Repository]AccessMode, len(accesses))
|
||||
for _, access := range accesses {
|
||||
repo, err := GetRepositoryByID(access.RepoID)
|
||||
var repos = make(map[*Repository]AccessMode, 10)
|
||||
var ownerCache = make(map[int64]*User, 10)
|
||||
for rows.Next() {
|
||||
var repo repoAccess
|
||||
err = rows.Scan(&repo)
|
||||
if err != nil {
|
||||
if IsErrRepoNotExist(err) {
|
||||
log.Error(4, "GetRepositoryByID: %v", err)
|
||||
continue
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if repo.Owner, ok = ownerCache[repo.OwnerID]; !ok {
|
||||
if err = repo.GetOwner(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
ownerCache[repo.OwnerID] = repo.Owner
|
||||
}
|
||||
if err = repo.GetOwner(); err != nil {
|
||||
return nil, err
|
||||
} else if repo.OwnerID == user.ID {
|
||||
continue
|
||||
}
|
||||
repos[repo] = access.Mode
|
||||
|
||||
repos[&repo.Repository] = repo.Access.Mode
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
@@ -154,7 +166,7 @@ func maxAccessMode(modes ...AccessMode) AccessMode {
|
||||
return max
|
||||
}
|
||||
|
||||
// FIXME: do corss-comparison so reduce deletions and additions to the minimum?
|
||||
// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
|
||||
func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode) (err error) {
|
||||
minMode := AccessModeRead
|
||||
if !repo.IsPrivate {
|
||||
|
||||
125
models/access_test.go
Normal file
125
models/access_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var accessModes = []AccessMode{
|
||||
AccessModeRead,
|
||||
AccessModeWrite,
|
||||
AccessModeAdmin,
|
||||
AccessModeOwner,
|
||||
}
|
||||
|
||||
func TestAccessLevel(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 2, IsPrivate: false}).(*Repository)
|
||||
repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository)
|
||||
|
||||
level, err := AccessLevel(user1.ID, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeOwner, level)
|
||||
|
||||
level, err = AccessLevel(user1.ID, repo2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeWrite, level)
|
||||
|
||||
level, err = AccessLevel(user2.ID, repo1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeRead, level)
|
||||
|
||||
level, err = AccessLevel(user2.ID, repo2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, AccessModeNone, level)
|
||||
}
|
||||
|
||||
func TestHasAccess(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 2, IsPrivate: false}).(*Repository)
|
||||
repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository)
|
||||
|
||||
for _, accessMode := range accessModes {
|
||||
has, err := HasAccess(user1.ID, repo1, accessMode)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
|
||||
has, err = HasAccess(user1.ID, repo2, accessMode)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accessMode <= AccessModeWrite, has)
|
||||
|
||||
has, err = HasAccess(user2.ID, repo1, accessMode)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accessMode <= AccessModeRead, has)
|
||||
|
||||
has, err = HasAccess(user2.ID, repo2, accessMode)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accessMode <= AccessModeNone, has)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_GetRepositoryAccesses(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user1 := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
|
||||
accesses, err := user1.GetRepositoryAccesses()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, accesses, 0)
|
||||
}
|
||||
|
||||
func TestUser_GetAccessibleRepositories(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user1 := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
|
||||
repos, err := user1.GetAccessibleRepositories(0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repos, 0)
|
||||
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repos, err = user2.GetAccessibleRepositories(0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repos, 1)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses(t *testing.T) {
|
||||
// test with organization repo
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
|
||||
assert.NoError(t, repo1.GetOwner())
|
||||
|
||||
_, err := x.Delete(&Collaboration{UserID: 2, RepoID: 3})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, repo1.RecalculateAccesses())
|
||||
|
||||
access := &Access{UserID: 2, RepoID: 3}
|
||||
has, err := x.Get(access)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
assert.Equal(t, AccessModeOwner, access.Mode)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses2(t *testing.T) {
|
||||
// test with non-organization repo
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 4}).(*Repository)
|
||||
assert.NoError(t, repo1.GetOwner())
|
||||
|
||||
_, err := x.Delete(&Collaboration{UserID: 4, RepoID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, repo1.RecalculateAccesses())
|
||||
|
||||
has, err := x.Get(&Access{UserID: 4, RepoID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
}
|
||||
@@ -71,19 +71,19 @@ func init() {
|
||||
// used in template render.
|
||||
type Action struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 // Receiver user id.
|
||||
UserID int64 `xorm:"INDEX"` // Receiver user id.
|
||||
OpType ActionType
|
||||
ActUserID int64 // Action user id.
|
||||
ActUserID int64 `xorm:"INDEX"` // Action user id.
|
||||
ActUserName string // Action user name.
|
||||
ActAvatar string `xorm:"-"`
|
||||
RepoID int64
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
RepoUserName string
|
||||
RepoName string
|
||||
RefName string
|
||||
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
Content string `xorm:"TEXT"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
CreatedUnix int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// BeforeInsert will be invoked by XORM before inserting a record
|
||||
@@ -145,7 +145,7 @@ func (a *Action) GetRepoPath() string {
|
||||
}
|
||||
|
||||
// ShortRepoPath returns the virtual path to the action repository
|
||||
// trimed to max 20 + 1 + 33 chars.
|
||||
// trimmed to max 20 + 1 + 33 chars.
|
||||
func (a *Action) ShortRepoPath() string {
|
||||
return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
|
||||
}
|
||||
@@ -360,7 +360,7 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
|
||||
|
||||
issue, err := GetIssueByRef(ref)
|
||||
if err != nil {
|
||||
if IsErrIssueNotExist(err) {
|
||||
if IsErrIssueNotExist(err) || err == errMissingIssueNumber {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
@@ -418,7 +418,7 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
|
||||
}
|
||||
}
|
||||
|
||||
// It is conflict to have close and reopen at same time, so refsMarkd doesn't need to reinit here.
|
||||
// It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
|
||||
for _, ref := range issueReopenKeywordsPat.FindAllString(c.Message, -1) {
|
||||
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
|
||||
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
|
||||
@@ -658,17 +658,14 @@ func GetFeeds(ctxUser *User, actorID, offset int64, isProfile bool) ([]*Action,
|
||||
And("is_private = ?", false).
|
||||
And("act_user_id = ?", ctxUser.ID)
|
||||
} else if actorID != -1 && ctxUser.IsOrganization() {
|
||||
// FIXME: only need to get IDs here, not all fields of repository.
|
||||
repos, _, err := ctxUser.GetUserRepositories(actorID, 1, ctxUser.NumRepos)
|
||||
env, err := ctxUser.AccessibleReposEnv(actorID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
|
||||
}
|
||||
repoIDs, err := env.RepoIDs(1, ctxUser.NumRepos)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserRepositories: %v", err)
|
||||
}
|
||||
|
||||
var repoIDs []int64
|
||||
for _, repo := range repos {
|
||||
repoIDs = append(repoIDs, repo.ID)
|
||||
}
|
||||
|
||||
if len(repoIDs) > 0 {
|
||||
sess.In("repo_id", repoIDs)
|
||||
}
|
||||
|
||||
337
models/action_test.go
Normal file
337
models/action_test.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAction_GetRepoPath(t *testing.T) {
|
||||
action := &Action{
|
||||
RepoUserName: "username",
|
||||
RepoName: "reponame",
|
||||
}
|
||||
assert.Equal(t, "username/reponame", action.GetRepoPath())
|
||||
}
|
||||
|
||||
func TestAction_GetRepoLink(t *testing.T) {
|
||||
action := &Action{
|
||||
RepoUserName: "username",
|
||||
RepoName: "reponame",
|
||||
}
|
||||
setting.AppSubURL = "/suburl/"
|
||||
assert.Equal(t, "/suburl/username/reponame", action.GetRepoLink())
|
||||
setting.AppSubURL = ""
|
||||
assert.Equal(t, "/username/reponame", action.GetRepoLink())
|
||||
}
|
||||
|
||||
func TestNewRepoAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{OwnerID: user.ID}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionCreateRepo,
|
||||
ActUserID: user.ID,
|
||||
RepoID: repo.ID,
|
||||
ActUserName: user.Name,
|
||||
RepoName: repo.Name,
|
||||
RepoUserName: repo.Owner.Name,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, NewRepoAction(user, repo))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestRenameRepoAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{OwnerID: user.ID}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
oldRepoName := repo.Name
|
||||
const newRepoName = "newRepoName"
|
||||
repo.Name = newRepoName
|
||||
repo.LowerName = strings.ToLower(newRepoName)
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionRenameRepo,
|
||||
ActUserID: user.ID,
|
||||
ActUserName: user.Name,
|
||||
RepoID: repo.ID,
|
||||
RepoName: repo.Name,
|
||||
RepoUserName: repo.Owner.Name,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: oldRepoName,
|
||||
}
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, RenameRepoAction(user, oldRepoName, repo))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
|
||||
_, err := x.Id(repo.ID).Cols("name", "lower_name").Update(repo)
|
||||
assert.NoError(t, err)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestPushCommits_ToAPIPayloadCommits(t *testing.T) {
|
||||
pushCommits := NewPushCommits()
|
||||
pushCommits.Commits = []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "message1",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "message2",
|
||||
},
|
||||
}
|
||||
pushCommits.Len = len(pushCommits.Commits)
|
||||
|
||||
payloadCommits := pushCommits.ToAPIPayloadCommits("/username/reponame")
|
||||
assert.Len(t, payloadCommits, 2)
|
||||
assert.Equal(t, "abcdef1", payloadCommits[0].ID)
|
||||
assert.Equal(t, "message1", payloadCommits[0].Message)
|
||||
assert.Equal(t, "/username/reponame/commit/abcdef1", payloadCommits[0].URL)
|
||||
assert.Equal(t, "User Two", payloadCommits[0].Committer.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[0].Committer.UserName)
|
||||
assert.Equal(t, "User Four", payloadCommits[0].Author.Name)
|
||||
assert.Equal(t, "user4", payloadCommits[0].Author.UserName)
|
||||
|
||||
assert.Equal(t, "abcdef2", payloadCommits[1].ID)
|
||||
assert.Equal(t, "message2", payloadCommits[1].Message)
|
||||
assert.Equal(t, "/username/reponame/commit/abcdef2", payloadCommits[1].URL)
|
||||
assert.Equal(t, "User Two", payloadCommits[1].Committer.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[1].Committer.UserName)
|
||||
assert.Equal(t, "User Two", payloadCommits[1].Author.Name)
|
||||
assert.Equal(t, "user2", payloadCommits[1].Author.UserName)
|
||||
}
|
||||
|
||||
func TestPushCommits_AvatarLink(t *testing.T) {
|
||||
pushCommits := NewPushCommits()
|
||||
pushCommits.Commits = []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "message1",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "message2",
|
||||
},
|
||||
}
|
||||
pushCommits.Len = len(pushCommits.Commits)
|
||||
|
||||
assert.Equal(t,
|
||||
"https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f",
|
||||
pushCommits.AvatarLink("user2@example.com"))
|
||||
|
||||
assert.Equal(t,
|
||||
"https://secure.gravatar.com/avatar/19ade630b94e1e0535b3df7387434154",
|
||||
pushCommits.AvatarLink("nonexistent@example.com"))
|
||||
}
|
||||
|
||||
func TestUpdateIssuesCommit(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
pushCommits := []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "start working on #1",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "a plain message",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "close #2",
|
||||
},
|
||||
}
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
commentBean := &Comment{
|
||||
Type: CommentTypeCommitRef,
|
||||
CommitSHA: "abcdef1",
|
||||
PosterID: user.ID,
|
||||
IssueID: 1,
|
||||
}
|
||||
issueBean := &Issue{RepoID: repo.ID, Index: 2}
|
||||
|
||||
AssertNotExistsBean(t, commentBean)
|
||||
AssertNotExistsBean(t, &Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
|
||||
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits))
|
||||
AssertExistsAndLoadBean(t, commentBean)
|
||||
AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestCommitRepoAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 2, OwnerID: user.ID}).(*Repository)
|
||||
repo.Owner = user
|
||||
|
||||
pushCommits := NewPushCommits()
|
||||
pushCommits.Commits = []*PushCommit{
|
||||
{
|
||||
Sha1: "abcdef1",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user4@example.com",
|
||||
AuthorName: "User Four",
|
||||
Message: "message1",
|
||||
},
|
||||
{
|
||||
Sha1: "abcdef2",
|
||||
CommitterEmail: "user2@example.com",
|
||||
CommitterName: "User Two",
|
||||
AuthorEmail: "user2@example.com",
|
||||
AuthorName: "User Two",
|
||||
Message: "message2",
|
||||
},
|
||||
}
|
||||
pushCommits.Len = len(pushCommits.Commits)
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionCommitRepo,
|
||||
ActUserID: user.ID,
|
||||
ActUserName: user.Name,
|
||||
RepoID: repo.ID,
|
||||
RepoName: repo.Name,
|
||||
RefName: "refName",
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, CommitRepoAction(CommitRepoActionOptions{
|
||||
PusherName: user.Name,
|
||||
RepoOwnerID: user.ID,
|
||||
RepoName: repo.Name,
|
||||
RefFullName: "refName",
|
||||
OldCommitID: "oldCommitID",
|
||||
NewCommitID: "newCommitID",
|
||||
Commits: pushCommits,
|
||||
}))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestTransferRepoAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1, OwnerID: user2.ID}).(*Repository)
|
||||
|
||||
repo.OwnerID = user4.ID
|
||||
repo.Owner = user4
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionTransferRepo,
|
||||
ActUserID: user2.ID,
|
||||
ActUserName: user2.Name,
|
||||
RepoID: repo.ID,
|
||||
RepoName: repo.Name,
|
||||
RepoUserName: repo.Owner.Name,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, TransferRepoAction(user2, user2, repo))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
|
||||
_, err := x.Id(repo.ID).Cols("owner_id").Update(repo)
|
||||
assert.NoError(t, err)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestMergePullRequestAction(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1, OwnerID: user.ID}).(*Repository)
|
||||
repo.Owner = user
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 3, RepoID: repo.ID}).(*Issue)
|
||||
|
||||
actionBean := &Action{
|
||||
OpType: ActionMergePullRequest,
|
||||
ActUserID: user.ID,
|
||||
ActUserName: user.Name,
|
||||
RepoID: repo.ID,
|
||||
RepoName: repo.Name,
|
||||
RepoUserName: repo.Owner.Name,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}
|
||||
AssertNotExistsBean(t, actionBean)
|
||||
assert.NoError(t, MergePullRequestAction(user, repo, issue))
|
||||
AssertExistsAndLoadBean(t, actionBean)
|
||||
CheckConsistencyFor(t, &Action{})
|
||||
}
|
||||
|
||||
func TestGetFeeds(t *testing.T) {
|
||||
// test with an individual user
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
actions, err := GetFeeds(user, user.ID, 0, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), actions[0].ID)
|
||||
assert.Equal(t, user.ID, actions[0].UserID)
|
||||
|
||||
actions, err = GetFeeds(user, user.ID, 0, true)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 0)
|
||||
}
|
||||
|
||||
func TestGetFeeds2(t *testing.T) {
|
||||
// test with an organization user
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
user := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
|
||||
|
||||
actions, err := GetFeeds(user, user.ID, 0, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(2), actions[0].ID)
|
||||
assert.Equal(t, user.ID, actions[0].UserID)
|
||||
|
||||
actions, err = GetFeeds(user, user.ID, 0, true)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 0)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ type Notice struct {
|
||||
Type NoticeType
|
||||
Description string `xorm:"TEXT"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
CreatedUnix int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// BeforeInsert is invoked from XORM before inserting an object of this type.
|
||||
@@ -55,27 +55,30 @@ func (n *Notice) TrStr() string {
|
||||
|
||||
// CreateNotice creates new system notice.
|
||||
func CreateNotice(tp NoticeType, desc string) error {
|
||||
// prevent panic if database connection is not available at this point
|
||||
if x == nil {
|
||||
return fmt.Errorf("Could not save notice due database connection not being available: %d %s", tp, desc)
|
||||
}
|
||||
return createNotice(x, tp, desc)
|
||||
}
|
||||
|
||||
func createNotice(e Engine, tp NoticeType, desc string) error {
|
||||
n := &Notice{
|
||||
Type: tp,
|
||||
Description: desc,
|
||||
}
|
||||
_, err := x.Insert(n)
|
||||
_, err := e.Insert(n)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateRepositoryNotice creates new system notice with type NoticeRepository.
|
||||
func CreateRepositoryNotice(desc string) error {
|
||||
return CreateNotice(NoticeRepository, desc)
|
||||
return createNotice(x, NoticeRepository, desc)
|
||||
}
|
||||
|
||||
// RemoveAllWithNotice removes all directories in given path and
|
||||
// creates a system notice when error occurs.
|
||||
func RemoveAllWithNotice(title, path string) {
|
||||
removeAllWithNotice(x, title, path)
|
||||
}
|
||||
|
||||
func removeAllWithNotice(e Engine, title, path string) {
|
||||
var err error
|
||||
// workaround for Go not being able to remove read-only files/folders: https://github.com/golang/go/issues/9606
|
||||
// this bug should be fixed on Go 1.7, so the workaround should be removed when Gogs don't support Go 1.6 anymore:
|
||||
@@ -91,7 +94,7 @@ func RemoveAllWithNotice(title, path string) {
|
||||
if err != nil {
|
||||
desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
|
||||
log.Warn(desc)
|
||||
if err = CreateRepositoryNotice(desc); err != nil {
|
||||
if err = createNotice(e, NoticeRepository, desc); err != nil {
|
||||
log.Error(4, "CreateRepositoryNotice: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -103,7 +106,7 @@ func CountNotices() int64 {
|
||||
return count
|
||||
}
|
||||
|
||||
// Notices returns number of notices in given page.
|
||||
// Notices returns notices in given page.
|
||||
func Notices(page, pageSize int) ([]*Notice, error) {
|
||||
notices := make([]*Notice, 0, pageSize)
|
||||
return notices, x.
|
||||
|
||||
111
models/admin_test.go
Normal file
111
models/admin_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNotice_TrStr(t *testing.T) {
|
||||
notice := &Notice{
|
||||
Type: NoticeRepository,
|
||||
Description: "test description",
|
||||
}
|
||||
assert.Equal(t, "admin.notices.type_1", notice.TrStr())
|
||||
}
|
||||
|
||||
func TestCreateNotice(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
noticeBean := &Notice{
|
||||
Type: NoticeRepository,
|
||||
Description: "test description",
|
||||
}
|
||||
AssertNotExistsBean(t, noticeBean)
|
||||
assert.NoError(t, CreateNotice(noticeBean.Type, noticeBean.Description))
|
||||
AssertExistsAndLoadBean(t, noticeBean)
|
||||
}
|
||||
|
||||
func TestCreateRepositoryNotice(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
noticeBean := &Notice{
|
||||
Type: NoticeRepository,
|
||||
Description: "test description",
|
||||
}
|
||||
AssertNotExistsBean(t, noticeBean)
|
||||
assert.NoError(t, CreateRepositoryNotice(noticeBean.Description))
|
||||
AssertExistsAndLoadBean(t, noticeBean)
|
||||
}
|
||||
|
||||
// TODO TestRemoveAllWithNotice
|
||||
|
||||
func TestCountNotices(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
assert.Equal(t, int64(3), CountNotices())
|
||||
}
|
||||
|
||||
func TestNotices(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
notices, err := Notices(1, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, notices, 2)
|
||||
assert.Equal(t, int64(3), notices[0].ID)
|
||||
assert.Equal(t, int64(2), notices[1].ID)
|
||||
|
||||
notices, err = Notices(2, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, notices, 1)
|
||||
assert.Equal(t, int64(1), notices[0].ID)
|
||||
}
|
||||
|
||||
func TestDeleteNotice(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 3})
|
||||
assert.NoError(t, DeleteNotice(3))
|
||||
AssertNotExistsBean(t, &Notice{ID: 3})
|
||||
}
|
||||
|
||||
func TestDeleteNotices(t *testing.T) {
|
||||
// delete a non-empty range
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 1})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 2})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 3})
|
||||
assert.NoError(t, DeleteNotices(1, 2))
|
||||
AssertNotExistsBean(t, &Notice{ID: 1})
|
||||
AssertNotExistsBean(t, &Notice{ID: 2})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 3})
|
||||
}
|
||||
|
||||
func TestDeleteNotices2(t *testing.T) {
|
||||
// delete an empty range
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 1})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 2})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 3})
|
||||
assert.NoError(t, DeleteNotices(3, 2))
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 1})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 2})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 3})
|
||||
}
|
||||
|
||||
func TestDeleteNoticesByIDs(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 1})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 2})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 3})
|
||||
assert.NoError(t, DeleteNoticesByIDs([]int64{1, 3}))
|
||||
AssertNotExistsBean(t, &Notice{ID: 1})
|
||||
AssertExistsAndLoadBean(t, &Notice{ID: 2})
|
||||
AssertNotExistsBean(t, &Notice{ID: 3})
|
||||
}
|
||||
175
models/attachment.go
Normal file
175
models/attachment.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
gouuid "github.com/satori/go.uuid"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Attachment represent a attachment of issue/comment/release.
|
||||
type Attachment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UUID string `xorm:"uuid UNIQUE"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
CommentID int64
|
||||
ReleaseID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// BeforeInsert is invoked from XORM before inserting an object of this type.
|
||||
func (a *Attachment) BeforeInsert() {
|
||||
a.CreatedUnix = time.Now().Unix()
|
||||
}
|
||||
|
||||
// AfterSet is invoked from XORM after setting the value of a field of
|
||||
// this object.
|
||||
func (a *Attachment) AfterSet(colName string, _ xorm.Cell) {
|
||||
switch colName {
|
||||
case "created_unix":
|
||||
a.Created = time.Unix(a.CreatedUnix, 0).Local()
|
||||
}
|
||||
}
|
||||
|
||||
// AttachmentLocalPath returns where attachment is stored in local file
|
||||
// system based on given UUID.
|
||||
func AttachmentLocalPath(uuid string) string {
|
||||
return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
|
||||
}
|
||||
|
||||
// LocalPath returns where attachment is stored in local file system.
|
||||
func (a *Attachment) LocalPath() string {
|
||||
return AttachmentLocalPath(a.UUID)
|
||||
}
|
||||
|
||||
// NewAttachment creates a new attachment object.
|
||||
func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) {
|
||||
attach := &Attachment{
|
||||
UUID: gouuid.NewV4().String(),
|
||||
Name: name,
|
||||
}
|
||||
|
||||
localPath := attach.LocalPath()
|
||||
if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil {
|
||||
return nil, fmt.Errorf("MkdirAll: %v", err)
|
||||
}
|
||||
|
||||
fw, err := os.Create(localPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Create: %v", err)
|
||||
}
|
||||
defer fw.Close()
|
||||
|
||||
if _, err = fw.Write(buf); err != nil {
|
||||
return nil, fmt.Errorf("Write: %v", err)
|
||||
} else if _, err = io.Copy(fw, file); err != nil {
|
||||
return nil, fmt.Errorf("Copy: %v", err)
|
||||
}
|
||||
|
||||
if _, err := x.Insert(attach); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return attach, nil
|
||||
}
|
||||
|
||||
func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
|
||||
attach := &Attachment{UUID: uuid}
|
||||
has, err := e.Get(attach)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrAttachmentNotExist{0, uuid}
|
||||
}
|
||||
return attach, nil
|
||||
}
|
||||
|
||||
func getAttachmentsByUUIDs(e Engine, uuids []string) ([]*Attachment, error) {
|
||||
if len(uuids) == 0 {
|
||||
return []*Attachment{}, nil
|
||||
}
|
||||
|
||||
// Silently drop invalid uuids.
|
||||
attachments := make([]*Attachment, 0, len(uuids))
|
||||
return attachments, e.In("uuid", uuids).Find(&attachments)
|
||||
}
|
||||
|
||||
// GetAttachmentByUUID returns attachment by given UUID.
|
||||
func GetAttachmentByUUID(uuid string) (*Attachment, error) {
|
||||
return getAttachmentByUUID(x, uuid)
|
||||
}
|
||||
|
||||
func getAttachmentsByIssueID(e Engine, issueID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
return attachments, e.Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
|
||||
}
|
||||
|
||||
// GetAttachmentsByIssueID returns all attachments of an issue.
|
||||
func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) {
|
||||
return getAttachmentsByIssueID(x, issueID)
|
||||
}
|
||||
|
||||
// GetAttachmentsByCommentID returns all attachments if comment by given ID.
|
||||
func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
|
||||
}
|
||||
|
||||
// DeleteAttachment deletes the given attachment and optionally the associated file.
|
||||
func DeleteAttachment(a *Attachment, remove bool) error {
|
||||
_, err := DeleteAttachments([]*Attachment{a}, remove)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteAttachments deletes the given attachments and optionally the associated files.
|
||||
func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
|
||||
for i, a := range attachments {
|
||||
if remove {
|
||||
if err := os.Remove(a.LocalPath()); err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := x.Delete(a); err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
return len(attachments), nil
|
||||
}
|
||||
|
||||
// DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
|
||||
func DeleteAttachmentsByIssue(issueID int64, remove bool) (int, error) {
|
||||
attachments, err := GetAttachmentsByIssueID(issueID)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return DeleteAttachments(attachments, remove)
|
||||
}
|
||||
|
||||
// DeleteAttachmentsByComment deletes all attachments associated with the given comment.
|
||||
func DeleteAttachmentsByComment(commentID int64, remove bool) (int, error) {
|
||||
attachments, err := GetAttachmentsByCommentID(commentID)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return DeleteAttachments(attachments, remove)
|
||||
}
|
||||
156
models/branches.go
Normal file
156
models/branches.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2016 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// ProtectedBranchRepoID protected Repo ID
|
||||
ProtectedBranchRepoID = "GITEA_REPO_ID"
|
||||
)
|
||||
|
||||
// ProtectedBranch struct
|
||||
type ProtectedBranch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
BranchName string `xorm:"UNIQUE(s)"`
|
||||
CanPush bool
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
Updated time.Time `xorm:"-"`
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
// BeforeInsert before protected branch insert create and update time
|
||||
func (protectBranch *ProtectedBranch) BeforeInsert() {
|
||||
protectBranch.CreatedUnix = time.Now().Unix()
|
||||
protectBranch.UpdatedUnix = protectBranch.CreatedUnix
|
||||
}
|
||||
|
||||
// BeforeUpdate before protected branch update time
|
||||
func (protectBranch *ProtectedBranch) BeforeUpdate() {
|
||||
protectBranch.UpdatedUnix = time.Now().Unix()
|
||||
}
|
||||
|
||||
// GetProtectedBranchByRepoID getting protected branch by repo ID
|
||||
func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
|
||||
protectedBranches := make([]*ProtectedBranch, 0)
|
||||
return protectedBranches, x.Where("repo_id = ?", RepoID).Desc("updated_unix").Find(&protectedBranches)
|
||||
}
|
||||
|
||||
// GetProtectedBranchBy getting protected branch by ID/Name
|
||||
func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, error) {
|
||||
rel := &ProtectedBranch{RepoID: repoID, BranchName: strings.ToLower(BranchName)}
|
||||
has, err := x.Get(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// GetProtectedBranches get all protected branches
|
||||
func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
|
||||
protectedBranches := make([]*ProtectedBranch, 0)
|
||||
return protectedBranches, x.Find(&protectedBranches, &ProtectedBranch{RepoID: repo.ID})
|
||||
}
|
||||
|
||||
// AddProtectedBranch add protection to branch
|
||||
func (repo *Repository) AddProtectedBranch(branchName string, canPush bool) error {
|
||||
protectedBranch := &ProtectedBranch{
|
||||
RepoID: repo.ID,
|
||||
BranchName: branchName,
|
||||
}
|
||||
|
||||
has, err := x.Get(protectedBranch)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return nil
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
protectedBranch.CanPush = canPush
|
||||
if _, err = sess.InsertOne(protectedBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// ChangeProtectedBranch access mode sets new access mode for the ProtectedBranch.
|
||||
func (repo *Repository) ChangeProtectedBranch(id int64, canPush bool) error {
|
||||
ProtectedBranch := &ProtectedBranch{
|
||||
RepoID: repo.ID,
|
||||
ID: id,
|
||||
}
|
||||
has, err := x.Get(ProtectedBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get ProtectedBranch: %v", err)
|
||||
} else if !has {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ProtectedBranch.CanPush == canPush {
|
||||
return nil
|
||||
}
|
||||
ProtectedBranch.CanPush = canPush
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Id(ProtectedBranch.ID).AllCols().Update(ProtectedBranch); err != nil {
|
||||
return fmt.Errorf("update ProtectedBranch: %v", err)
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
|
||||
func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
|
||||
protectedBranch := &ProtectedBranch{
|
||||
RepoID: repo.ID,
|
||||
ID: id,
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if affected, err := sess.Delete(protectedBranch); err != nil {
|
||||
return err
|
||||
} else if affected != 1 {
|
||||
return fmt.Errorf("delete protected branch ID(%v) failed", id)
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// newProtectedBranch insert one queue
|
||||
func newProtectedBranch(protectedBranch *ProtectedBranch) error {
|
||||
_, err := x.InsertOne(protectedBranch)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateProtectedBranch update queue
|
||||
func UpdateProtectedBranch(protectedBranch *ProtectedBranch) error {
|
||||
_, err := x.Update(protectedBranch)
|
||||
return err
|
||||
}
|
||||
172
models/consistency_test.go
Normal file
172
models/consistency_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// ConsistencyCheckable a type that can be tested for database consistency
|
||||
type ConsistencyCheckable interface {
|
||||
CheckForConsistency(t *testing.T)
|
||||
}
|
||||
|
||||
// CheckConsistencyForAll test that the entire database is consistent
|
||||
func CheckConsistencyForAll(t *testing.T) {
|
||||
CheckConsistencyFor(t,
|
||||
&User{},
|
||||
&Repository{},
|
||||
&Issue{},
|
||||
&PullRequest{},
|
||||
&Milestone{},
|
||||
&Label{},
|
||||
&Team{},
|
||||
&Action{})
|
||||
}
|
||||
|
||||
// CheckConsistencyFor test that all matching database entries are consistent
|
||||
func CheckConsistencyFor(t *testing.T, beansToCheck ...interface{}) {
|
||||
for _, bean := range beansToCheck {
|
||||
sliceType := reflect.SliceOf(reflect.TypeOf(bean))
|
||||
sliceValue := reflect.MakeSlice(sliceType, 0, 10)
|
||||
|
||||
ptrToSliceValue := reflect.New(sliceType)
|
||||
ptrToSliceValue.Elem().Set(sliceValue)
|
||||
|
||||
assert.NoError(t, x.Where(bean).Find(ptrToSliceValue.Interface()))
|
||||
sliceValue = ptrToSliceValue.Elem()
|
||||
|
||||
for i := 0; i < sliceValue.Len(); i++ {
|
||||
entity := sliceValue.Index(i).Interface()
|
||||
checkable, ok := entity.(ConsistencyCheckable)
|
||||
if !ok {
|
||||
t.Errorf("Expected %+v (of type %T) to be checkable for consistency",
|
||||
entity, entity)
|
||||
} else {
|
||||
checkable.CheckForConsistency(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getCount get the count of database entries matching bean
|
||||
func getCount(t *testing.T, e Engine, bean interface{}) int64 {
|
||||
count, err := e.Count(bean)
|
||||
assert.NoError(t, err)
|
||||
return count
|
||||
}
|
||||
|
||||
// assertCount test the count of database entries matching bean
|
||||
func assertCount(t *testing.T, bean interface{}, expected int) {
|
||||
assert.EqualValues(t, expected, getCount(t, x, bean),
|
||||
"Failed consistency test, the counted bean (of type %T) was %+v", bean, bean)
|
||||
}
|
||||
|
||||
func (user *User) CheckForConsistency(t *testing.T) {
|
||||
assertCount(t, &Repository{OwnerID: user.ID}, user.NumRepos)
|
||||
assertCount(t, &Star{UID: user.ID}, user.NumStars)
|
||||
assertCount(t, &OrgUser{OrgID: user.ID}, user.NumMembers)
|
||||
assertCount(t, &Team{OrgID: user.ID}, user.NumTeams)
|
||||
assertCount(t, &Follow{UserID: user.ID}, user.NumFollowing)
|
||||
assertCount(t, &Follow{FollowID: user.ID}, user.NumFollowers)
|
||||
if user.Type != UserTypeOrganization {
|
||||
assert.EqualValues(t, 0, user.NumMembers)
|
||||
assert.EqualValues(t, 0, user.NumTeams)
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *Repository) CheckForConsistency(t *testing.T) {
|
||||
assert.Equal(t, repo.LowerName, strings.ToLower(repo.Name), "repo: %+v", repo)
|
||||
assertCount(t, &Star{RepoID: repo.ID}, repo.NumStars)
|
||||
assertCount(t, &Watch{RepoID: repo.ID}, repo.NumWatches)
|
||||
assertCount(t, &Milestone{RepoID: repo.ID}, repo.NumMilestones)
|
||||
assertCount(t, &Repository{ForkID: repo.ID}, repo.NumForks)
|
||||
if repo.IsFork {
|
||||
AssertExistsAndLoadBean(t, &Repository{ID: repo.ForkID})
|
||||
}
|
||||
|
||||
actual := getCount(t, x.Where("is_pull=?", false), &Issue{RepoID: repo.ID})
|
||||
assert.EqualValues(t, repo.NumIssues, actual,
|
||||
"Unexpected number of issues for repo %+v", repo)
|
||||
|
||||
actual = getCount(t, x.Where("is_pull=? AND is_closed=?", false, true), &Issue{RepoID: repo.ID})
|
||||
assert.EqualValues(t, repo.NumClosedIssues, actual,
|
||||
"Unexpected number of closed issues for repo %+v", repo)
|
||||
|
||||
actual = getCount(t, x.Where("is_pull=?", true), &Issue{RepoID: repo.ID})
|
||||
assert.EqualValues(t, repo.NumPulls, actual,
|
||||
"Unexpected number of pulls for repo %+v", repo)
|
||||
|
||||
actual = getCount(t, x.Where("is_pull=? AND is_closed=?", true, true), &Issue{RepoID: repo.ID})
|
||||
assert.EqualValues(t, repo.NumClosedPulls, actual,
|
||||
"Unexpected number of closed pulls for repo %+v", repo)
|
||||
|
||||
actual = getCount(t, x.Where("is_closed=?", true), &Milestone{RepoID: repo.ID})
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, actual,
|
||||
"Unexpected number of closed milestones for repo %+v", repo)
|
||||
}
|
||||
|
||||
func (issue *Issue) CheckForConsistency(t *testing.T) {
|
||||
actual := getCount(t, x.Where("type=?", CommentTypeComment), &Comment{IssueID: issue.ID})
|
||||
assert.EqualValues(t, issue.NumComments, actual,
|
||||
"Unexpected number of comments for issue %+v", issue)
|
||||
if issue.IsPull {
|
||||
pr := AssertExistsAndLoadBean(t, &PullRequest{IssueID: issue.ID}).(*PullRequest)
|
||||
assert.EqualValues(t, pr.Index, issue.Index)
|
||||
}
|
||||
}
|
||||
|
||||
func (pr *PullRequest) CheckForConsistency(t *testing.T) {
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: pr.IssueID}).(*Issue)
|
||||
assert.True(t, issue.IsPull)
|
||||
assert.EqualValues(t, issue.Index, pr.Index)
|
||||
}
|
||||
|
||||
func (milestone *Milestone) CheckForConsistency(t *testing.T) {
|
||||
assertCount(t, &Issue{MilestoneID: milestone.ID}, milestone.NumIssues)
|
||||
|
||||
actual := getCount(t, x.Where("is_closed=?", true), &Issue{MilestoneID: milestone.ID})
|
||||
assert.EqualValues(t, milestone.NumClosedIssues, actual,
|
||||
"Unexpected number of closed issues for milestone %+v", milestone)
|
||||
}
|
||||
|
||||
func (label *Label) CheckForConsistency(t *testing.T) {
|
||||
issueLabels := make([]*IssueLabel, 0, 10)
|
||||
assert.NoError(t, x.Find(&issueLabels, &IssueLabel{LabelID: label.ID}))
|
||||
assert.EqualValues(t, label.NumIssues, len(issueLabels),
|
||||
"Unexpected number of issue for label %+v", label)
|
||||
|
||||
issueIDs := make([]int64, len(issueLabels))
|
||||
for i, issueLabel := range issueLabels {
|
||||
issueIDs[i] = issueLabel.IssueID
|
||||
}
|
||||
|
||||
expected := int64(0)
|
||||
if len(issueIDs) > 0 {
|
||||
expected = getCount(t, x.In("id", issueIDs).Where("is_closed=?", true), &Issue{})
|
||||
}
|
||||
assert.EqualValues(t, expected, label.NumClosedIssues,
|
||||
"Unexpected number of closed issues for label %+v", label)
|
||||
}
|
||||
|
||||
func (team *Team) CheckForConsistency(t *testing.T) {
|
||||
assertCount(t, &TeamUser{TeamID: team.ID}, team.NumMembers)
|
||||
assertCount(t, &TeamRepo{TeamID: team.ID}, team.NumRepos)
|
||||
}
|
||||
|
||||
func (action *Action) CheckForConsistency(t *testing.T) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: action.RepoID}).(*Repository)
|
||||
owner := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
|
||||
actor := AssertExistsAndLoadBean(t, &User{ID: action.ActUserID}).(*User)
|
||||
|
||||
assert.Equal(t, repo.Name, action.RepoName, "action: %+v", action)
|
||||
assert.Equal(t, repo.IsPrivate, action.IsPrivate, "action: %+v", action)
|
||||
assert.Equal(t, owner.Name, action.RepoUserName, "action: %+v", action)
|
||||
assert.Equal(t, actor.Name, action.ActUserName, "action: %+v", action)
|
||||
}
|
||||
164
models/error.go
164
models/error.go
@@ -93,6 +93,21 @@ func (err ErrEmailAlreadyUsed) Error() string {
|
||||
return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
|
||||
}
|
||||
|
||||
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
|
||||
type ErrOpenIDAlreadyUsed struct {
|
||||
OpenID string
|
||||
}
|
||||
|
||||
// IsErrOpenIDAlreadyUsed checks if an error is a ErrOpenIDAlreadyUsed.
|
||||
func IsErrOpenIDAlreadyUsed(err error) bool {
|
||||
_, ok := err.(ErrOpenIDAlreadyUsed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrOpenIDAlreadyUsed) Error() string {
|
||||
return fmt.Sprintf("OpenID has been used [oid: %s]", err.OpenID)
|
||||
}
|
||||
|
||||
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
|
||||
type ErrUserOwnRepos struct {
|
||||
UID int64
|
||||
@@ -123,6 +138,20 @@ func (err ErrUserHasOrgs) Error() string {
|
||||
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// ErrUserNotAllowedCreateOrg represents a "UserNotAllowedCreateOrg" kind of error.
|
||||
type ErrUserNotAllowedCreateOrg struct {
|
||||
}
|
||||
|
||||
// IsErrUserNotAllowedCreateOrg checks if an error is an ErrUserNotAllowedCreateOrg.
|
||||
func IsErrUserNotAllowedCreateOrg(err error) bool {
|
||||
_, ok := err.(ErrUserNotAllowedCreateOrg)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserNotAllowedCreateOrg) Error() string {
|
||||
return fmt.Sprintf("user is not allowed to create organizations")
|
||||
}
|
||||
|
||||
// ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error.
|
||||
type ErrReachLimitOfRepo struct {
|
||||
Limit int
|
||||
@@ -199,8 +228,9 @@ func (err ErrKeyNotExist) Error() string {
|
||||
|
||||
// ErrKeyAlreadyExist represents a "KeyAlreadyExist" kind of error.
|
||||
type ErrKeyAlreadyExist struct {
|
||||
OwnerID int64
|
||||
Content string
|
||||
OwnerID int64
|
||||
Fingerprint string
|
||||
Content string
|
||||
}
|
||||
|
||||
// IsErrKeyAlreadyExist checks if an error is a ErrKeyAlreadyExist.
|
||||
@@ -210,7 +240,8 @@ func IsErrKeyAlreadyExist(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrKeyAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content)
|
||||
return fmt.Sprintf("public key already exists [owner_id: %d, finter_print: %s, content: %s]",
|
||||
err.OwnerID, err.Fingerprint, err.Content)
|
||||
}
|
||||
|
||||
// ErrKeyNameAlreadyUsed represents a "KeyNameAlreadyUsed" kind of error.
|
||||
@@ -229,6 +260,54 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
|
||||
return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
|
||||
}
|
||||
|
||||
// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
|
||||
type ErrGPGKeyNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrGPGKeyNotExist checks if an error is a ErrGPGKeyNotExist.
|
||||
func IsErrGPGKeyNotExist(err error) bool {
|
||||
_, ok := err.(ErrGPGKeyNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGPGKeyNotExist) Error() string {
|
||||
return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error.
|
||||
type ErrGPGKeyIDAlreadyUsed struct {
|
||||
KeyID string
|
||||
}
|
||||
|
||||
// IsErrGPGKeyIDAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed.
|
||||
func IsErrGPGKeyIDAlreadyUsed(err error) bool {
|
||||
_, ok := err.(ErrGPGKeyIDAlreadyUsed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGPGKeyIDAlreadyUsed) Error() string {
|
||||
return fmt.Sprintf("public key already exists [key_id: %s]", err.KeyID)
|
||||
}
|
||||
|
||||
// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
|
||||
type ErrGPGKeyAccessDenied struct {
|
||||
UserID int64
|
||||
KeyID int64
|
||||
}
|
||||
|
||||
// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
|
||||
func IsErrGPGKeyAccessDenied(err error) bool {
|
||||
_, ok := err.(ErrGPGKeyAccessDenied)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
|
||||
func (err ErrGPGKeyAccessDenied) Error() string {
|
||||
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
|
||||
err.UserID, err.KeyID)
|
||||
}
|
||||
|
||||
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
|
||||
type ErrKeyAccessDenied struct {
|
||||
UserID int64
|
||||
@@ -394,6 +473,22 @@ func (err ErrRepoAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
|
||||
}
|
||||
|
||||
// ErrRepoRedirectNotExist represents a "RepoRedirectNotExist" kind of error.
|
||||
type ErrRepoRedirectNotExist struct {
|
||||
OwnerID int64
|
||||
RepoName string
|
||||
}
|
||||
|
||||
// IsErrRepoRedirectNotExist check if an error is an ErrRepoRedirectNotExist
|
||||
func IsErrRepoRedirectNotExist(err error) bool {
|
||||
_, ok := err.(ErrRepoRedirectNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrRepoRedirectNotExist) Error() string {
|
||||
return fmt.Sprintf("repository redirect does not exist [uid: %d, name: %s]", err.OwnerID, err.RepoName)
|
||||
}
|
||||
|
||||
// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error.
|
||||
type ErrInvalidCloneAddr struct {
|
||||
IsURLError bool
|
||||
@@ -569,7 +664,7 @@ type ErrPullRequestNotExist struct {
|
||||
IssueID int64
|
||||
HeadRepoID int64
|
||||
BaseRepoID int64
|
||||
HeadBarcnh string
|
||||
HeadBranch string
|
||||
BaseBranch string
|
||||
}
|
||||
|
||||
@@ -581,7 +676,7 @@ func IsErrPullRequestNotExist(err error) bool {
|
||||
|
||||
func (err ErrPullRequestNotExist) Error() string {
|
||||
return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
|
||||
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBarcnh, err.BaseBranch)
|
||||
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
|
||||
}
|
||||
|
||||
// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
|
||||
@@ -773,6 +868,25 @@ func (err ErrTeamAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
|
||||
}
|
||||
|
||||
//
|
||||
// Two-factor authentication
|
||||
//
|
||||
|
||||
// ErrTwoFactorNotEnrolled indicates that a user is not enrolled in two-factor authentication.
|
||||
type ErrTwoFactorNotEnrolled struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrTwoFactorNotEnrolled checks if an error is a ErrTwoFactorNotEnrolled.
|
||||
func IsErrTwoFactorNotEnrolled(err error) bool {
|
||||
_, ok := err.(ErrTwoFactorNotEnrolled)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTwoFactorNotEnrolled) Error() string {
|
||||
return fmt.Sprintf("user not enrolled in 2FA [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// ____ ___ .__ .___
|
||||
// | | \______ | | _________ __| _/
|
||||
// | | /\____ \| | / _ \__ \ / __ |
|
||||
@@ -796,3 +910,43 @@ func IsErrUploadNotExist(err error) bool {
|
||||
func (err ErrUploadNotExist) Error() string {
|
||||
return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
|
||||
}
|
||||
|
||||
// ___________ __ .__ .____ .__ ____ ___
|
||||
// \_ _____/__ ____/ |_ ___________ ____ _____ | | | | ____ ____ |__| ____ | | \______ ___________
|
||||
// | __)_\ \/ /\ __\/ __ \_ __ \/ \\__ \ | | | | / _ \ / ___\| |/ \ | | / ___// __ \_ __ \
|
||||
// | \> < | | \ ___/| | \/ | \/ __ \| |__ | |__( <_> ) /_/ > | | \ | | /\___ \\ ___/| | \/
|
||||
// /_______ /__/\_ \ |__| \___ >__| |___| (____ /____/ |_______ \____/\___ /|__|___| / |______//____ >\___ >__|
|
||||
// \/ \/ \/ \/ \/ \/ /_____/ \/ \/ \/
|
||||
|
||||
// ErrExternalLoginUserAlreadyExist represents a "ExternalLoginUserAlreadyExist" kind of error.
|
||||
type ErrExternalLoginUserAlreadyExist struct {
|
||||
ExternalID string
|
||||
UserID int64
|
||||
LoginSourceID int64
|
||||
}
|
||||
|
||||
// IsErrExternalLoginUserAlreadyExist checks if an error is a ExternalLoginUserAlreadyExist.
|
||||
func IsErrExternalLoginUserAlreadyExist(err error) bool {
|
||||
_, ok := err.(ErrExternalLoginUserAlreadyExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrExternalLoginUserAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID)
|
||||
}
|
||||
|
||||
// ErrExternalLoginUserNotExist represents a "ExternalLoginUserNotExist" kind of error.
|
||||
type ErrExternalLoginUserNotExist struct {
|
||||
UserID int64
|
||||
LoginSourceID int64
|
||||
}
|
||||
|
||||
// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist.
|
||||
func IsErrExternalLoginUserNotExist(err error) bool {
|
||||
_, ok := err.(ErrExternalLoginUserNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrExternalLoginUserNotExist) Error() string {
|
||||
return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
|
||||
}
|
||||
|
||||
74
models/external_login_user.go
Normal file
74
models/external_login_user.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import "github.com/markbates/goth"
|
||||
|
||||
// ExternalLoginUser makes the connecting between some existing user and additional external login sources
|
||||
type ExternalLoginUser struct {
|
||||
ExternalID string `xorm:"NOT NULL"`
|
||||
UserID int64 `xorm:"NOT NULL"`
|
||||
LoginSourceID int64 `xorm:"NOT NULL"`
|
||||
}
|
||||
|
||||
// GetExternalLogin checks if a externalID in loginSourceID scope already exists
|
||||
func GetExternalLogin(externalLoginUser *ExternalLoginUser) (bool, error) {
|
||||
return x.Get(externalLoginUser)
|
||||
}
|
||||
|
||||
// ListAccountLinks returns a map with the ExternalLoginUser and its LoginSource
|
||||
func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) {
|
||||
externalAccounts := make([]*ExternalLoginUser, 0, 5)
|
||||
err := x.Where("user_id=?", user.ID).
|
||||
Desc("login_source_id").
|
||||
Find(&externalAccounts)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return externalAccounts, nil
|
||||
}
|
||||
|
||||
// LinkAccountToUser link the gothUser to the user
|
||||
func LinkAccountToUser(user *User, gothUser goth.User) error {
|
||||
loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
externalLoginUser := &ExternalLoginUser{
|
||||
ExternalID: gothUser.UserID,
|
||||
UserID: user.ID,
|
||||
LoginSourceID: loginSource.ID,
|
||||
}
|
||||
has, err := x.Get(externalLoginUser)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID}
|
||||
}
|
||||
|
||||
_, err = x.Insert(externalLoginUser)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveAccountLink will remove all external login sources for the given user
|
||||
func RemoveAccountLink(user *User, loginSourceID int64) (int64, error) {
|
||||
deleted, err := x.Delete(&ExternalLoginUser{UserID: user.ID, LoginSourceID: loginSourceID})
|
||||
if err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
if deleted < 1 {
|
||||
return deleted, ErrExternalLoginUserNotExist{user.ID, loginSourceID}
|
||||
}
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
// removeAllAccountLinks will remove all external login sources for the given user
|
||||
func removeAllAccountLinks(e Engine, user *User) error {
|
||||
_, err := e.Delete(&ExternalLoginUser{UserID: user.ID})
|
||||
return err
|
||||
}
|
||||
11
models/fixtures/access.yml
Normal file
11
models/fixtures/access.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
-
|
||||
id: 1
|
||||
user_id: 2
|
||||
repo_id: 3
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 2
|
||||
user_id: 4
|
||||
repo_id: 4
|
||||
mode: 2 # write
|
||||
23
models/fixtures/access_token.yml
Normal file
23
models/fixtures/access_token.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 1
|
||||
name: Token A
|
||||
sha1: hash1
|
||||
created_unix: 946687980
|
||||
updated_unix: 946687980
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 1
|
||||
name: Token B
|
||||
sha1: hash2
|
||||
created_unix: 946687980
|
||||
updated_unix: 946687980
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 2
|
||||
name: Token A
|
||||
sha1: hash3
|
||||
created_unix: 946687980
|
||||
updated_unix: 946687980
|
||||
33
models/fixtures/action.yml
Normal file
33
models/fixtures/action.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
-
|
||||
id: 1
|
||||
user_id: 2
|
||||
op_type: 12 # close issue
|
||||
act_user_id: 2
|
||||
act_user_name: user2
|
||||
repo_id: 2
|
||||
repo_user_name: user2
|
||||
repo_name: repo2
|
||||
is_private: true
|
||||
|
||||
-
|
||||
id: 2
|
||||
user_id: 3
|
||||
op_type: 2 # rename repo
|
||||
act_user_id: 3
|
||||
act_user_name: user3
|
||||
repo_id: 3
|
||||
repo_user_name: user3
|
||||
repo_name: repo3
|
||||
is_private: true
|
||||
content: oldRepoName
|
||||
|
||||
-
|
||||
id: 3
|
||||
user_id: 11
|
||||
op_type: 1 # create repo
|
||||
act_user_id: 11
|
||||
act_user_name: user11
|
||||
repo_id: 9
|
||||
repo_user_name: user11
|
||||
repo_name: repo9
|
||||
is_private: false
|
||||
11
models/fixtures/collaboration.yml
Normal file
11
models/fixtures/collaboration.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 3
|
||||
user_id: 2
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 2
|
||||
repo_id: 4
|
||||
user_id: 4
|
||||
mode: 2 # write
|
||||
19
models/fixtures/comment.yml
Normal file
19
models/fixtures/comment.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
-
|
||||
id: 1
|
||||
type: 7 # label
|
||||
poster_id: 2
|
||||
issue_id: 1 # in repo_id 1
|
||||
label_id: 1
|
||||
content: "1"
|
||||
-
|
||||
id: 2
|
||||
type: 0 # comment
|
||||
poster_id: 3 # user not watching (see watch.yml)
|
||||
issue_id: 1 # in repo_id 1
|
||||
content: "good work!"
|
||||
-
|
||||
id: 3
|
||||
type: 0 # comment
|
||||
poster_id: 5 # user not watching (see watch.yml)
|
||||
issue_id: 1 # in repo_id 1
|
||||
content: "meh..."
|
||||
35
models/fixtures/email_address.yml
Normal file
35
models/fixtures/email_address.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 1
|
||||
email: user11@example.com
|
||||
is_activated: false
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 1
|
||||
email: user12@example.com
|
||||
is_activated: false
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 2
|
||||
email: user2@example.com
|
||||
is_activated: true
|
||||
|
||||
-
|
||||
id: 4
|
||||
uid: 2
|
||||
email: user21@example.com
|
||||
is_activated: false
|
||||
|
||||
-
|
||||
id: 5
|
||||
uid: 9999999
|
||||
email: user9999999@example.com
|
||||
is_activated: true
|
||||
|
||||
-
|
||||
id: 6
|
||||
uid: 10
|
||||
email: user101@example.com
|
||||
is_activated: true
|
||||
4
models/fixtures/follow.yml
Normal file
4
models/fixtures/follow.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
-
|
||||
id: 1
|
||||
user_id: 4
|
||||
follow_id: 2
|
||||
5
models/fixtures/hook_task.yml
Normal file
5
models/fixtures/hook_task.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
hook_id: 1
|
||||
uuid: uuid1
|
||||
59
models/fixtures/issue.yml
Normal file
59
models/fixtures/issue.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
index: 1
|
||||
poster_id: 1
|
||||
assignee_id: 1
|
||||
name: issue1
|
||||
content: content1
|
||||
is_closed: false
|
||||
is_pull: false
|
||||
num_comments: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 978307200
|
||||
|
||||
-
|
||||
id: 2
|
||||
repo_id: 1
|
||||
index: 2
|
||||
poster_id: 1
|
||||
name: issue2
|
||||
content: content2
|
||||
milestone_id: 1
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
created_unix: 946684810
|
||||
updated_unix: 978307190
|
||||
|
||||
|
||||
-
|
||||
id: 3
|
||||
repo_id: 1
|
||||
index: 3
|
||||
poster_id: 1
|
||||
name: issue3
|
||||
content: content4
|
||||
is_closed: false
|
||||
is_pull: true
|
||||
created_unix: 946684820
|
||||
updated_unix: 978307180
|
||||
|
||||
-
|
||||
id: 4
|
||||
repo_id: 2
|
||||
index: 1
|
||||
poster_id: 2
|
||||
name: issue4
|
||||
content: content4
|
||||
is_closed: true
|
||||
is_pull: false
|
||||
|
||||
-
|
||||
id: 5
|
||||
repo_id: 1
|
||||
index: 4
|
||||
poster_id: 2
|
||||
name: issue5
|
||||
content: content5
|
||||
is_closed: true
|
||||
is_pull: false
|
||||
14
models/fixtures/issue_label.yml
Normal file
14
models/fixtures/issue_label.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
-
|
||||
id: 1
|
||||
issue_id: 1
|
||||
label_id: 1
|
||||
|
||||
-
|
||||
id: 2
|
||||
issue_id: 5
|
||||
label_id: 2
|
||||
|
||||
-
|
||||
id: 3
|
||||
issue_id: 2
|
||||
label_id: 1
|
||||
23
models/fixtures/issue_user.yml
Normal file
23
models/fixtures/issue_user.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 1
|
||||
issue_id: 1
|
||||
is_read: true
|
||||
is_assigned: true
|
||||
is_mentioned: false
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 2
|
||||
issue_id: 1
|
||||
is_read: true
|
||||
is_assigned: false
|
||||
is_mentioned: false
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 4
|
||||
issue_id: 1
|
||||
is_read: false
|
||||
is_assigned: false
|
||||
is_mentioned: false
|
||||
15
models/fixtures/label.yml
Normal file
15
models/fixtures/label.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
name: label1
|
||||
color: '#abcdef'
|
||||
num_issues: 2
|
||||
num_closed_issues: 0
|
||||
|
||||
-
|
||||
id: 2
|
||||
repo_id: 1
|
||||
name: label2
|
||||
color: '#000000'
|
||||
num_issues: 1
|
||||
num_closed_issues: 1
|
||||
15
models/fixtures/milestone.yml
Normal file
15
models/fixtures/milestone.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
name: milestone1
|
||||
content: content1
|
||||
is_closed: false
|
||||
num_issues: 1
|
||||
|
||||
-
|
||||
id: 2
|
||||
repo_id: 1
|
||||
name: milestone2
|
||||
content: content2
|
||||
is_closed: false
|
||||
num_issues: 0
|
||||
14
models/fixtures/notice.yml
Normal file
14
models/fixtures/notice.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
-
|
||||
id: 1
|
||||
type: 1 # NoticeRepository
|
||||
description: description1
|
||||
|
||||
-
|
||||
id: 2
|
||||
type: 1 # NoticeRepository
|
||||
description: description2
|
||||
|
||||
-
|
||||
id: 3
|
||||
type: 1 # NoticeRepository
|
||||
description: description3
|
||||
21
models/fixtures/notification.yml
Normal file
21
models/fixtures/notification.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
-
|
||||
id: 1
|
||||
user_id: 1
|
||||
repo_id: 1
|
||||
status: 1 # unread
|
||||
source: 1 # issue
|
||||
updated_by: 2
|
||||
issue_id: 1
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
|
||||
-
|
||||
id: 2
|
||||
user_id: 2
|
||||
repo_id: 1
|
||||
status: 2 # read
|
||||
source: 1 # issue
|
||||
updated_by: 1
|
||||
issue_id: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
31
models/fixtures/org_user.yml
Normal file
31
models/fixtures/org_user.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 2
|
||||
org_id: 3
|
||||
is_public: true
|
||||
is_owner: true
|
||||
num_teams: 1
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 4
|
||||
org_id: 3
|
||||
is_public: false
|
||||
is_owner: false
|
||||
num_teams: 0
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 5
|
||||
org_id: 6
|
||||
is_public: true
|
||||
is_owner: true
|
||||
num_teams: 1
|
||||
|
||||
-
|
||||
id: 4
|
||||
uid: 5
|
||||
org_id: 7
|
||||
is_public: false
|
||||
is_owner: true
|
||||
num_teams: 1
|
||||
28
models/fixtures/pull_request.yml
Normal file
28
models/fixtures/pull_request.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
-
|
||||
id: 1
|
||||
type: 0 # gitea pull request
|
||||
status: 2 # mergable
|
||||
issue_id: 2
|
||||
index: 2
|
||||
head_repo_id: 1
|
||||
base_repo_id: 1
|
||||
head_user_name: user1
|
||||
head_branch: branch1
|
||||
base_branch: master
|
||||
merge_base: 1234567890abcdef
|
||||
has_merged: true
|
||||
merger_id: 2
|
||||
|
||||
-
|
||||
id: 2
|
||||
type: 0 # gitea pull request
|
||||
status: 1 # checking
|
||||
issue_id: 3
|
||||
index: 3
|
||||
head_repo_id: 1
|
||||
base_repo_id: 1
|
||||
head_user_name: user1
|
||||
head_branch: branch2
|
||||
base_branch: master
|
||||
merge_base: fedcba9876543210
|
||||
has_merged: false
|
||||
5
models/fixtures/repo_redirect.yml
Normal file
5
models/fixtures/repo_redirect.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
-
|
||||
id: 1
|
||||
owner_id: 2
|
||||
lower_name: oldrepo1
|
||||
redirect_repo_id: 1
|
||||
171
models/fixtures/repository.yml
Normal file
171
models/fixtures/repository.yml
Normal file
@@ -0,0 +1,171 @@
|
||||
-
|
||||
id: 1
|
||||
owner_id: 2
|
||||
lower_name: repo1
|
||||
name: repo1
|
||||
is_private: false
|
||||
num_issues: 2
|
||||
num_closed_issues: 1
|
||||
num_pulls: 2
|
||||
num_closed_pulls: 0
|
||||
num_milestones: 2
|
||||
num_watches: 2
|
||||
|
||||
-
|
||||
id: 2
|
||||
owner_id: 2
|
||||
lower_name: repo2
|
||||
name: repo2
|
||||
is_private: true
|
||||
num_issues: 1
|
||||
num_closed_issues: 1
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_stars: 1
|
||||
|
||||
-
|
||||
id: 3
|
||||
owner_id: 3
|
||||
lower_name: repo3
|
||||
name: repo3
|
||||
is_private: true
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_watches: 0
|
||||
|
||||
-
|
||||
id: 4
|
||||
owner_id: 5
|
||||
lower_name: repo4
|
||||
name: repo4
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_stars: 1
|
||||
|
||||
-
|
||||
id: 5
|
||||
owner_id: 3
|
||||
lower_name: repo5
|
||||
name: repo5
|
||||
is_private: true
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_watches: 0
|
||||
is_mirror: true
|
||||
|
||||
-
|
||||
id: 6
|
||||
owner_id: 10
|
||||
lower_name: repo6
|
||||
name: repo6
|
||||
is_private: true
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 7
|
||||
owner_id: 10
|
||||
lower_name: repo7
|
||||
name: repo7
|
||||
is_private: true
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 8
|
||||
owner_id: 10
|
||||
lower_name: repo8
|
||||
name: repo8
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 9
|
||||
owner_id: 11
|
||||
lower_name: repo9
|
||||
name: repo9
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 10
|
||||
owner_id: 12
|
||||
lower_name: repo10
|
||||
name: repo10
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
num_forks: 1
|
||||
|
||||
-
|
||||
id: 11
|
||||
fork_id: 10
|
||||
owner_id: 13
|
||||
lower_name: repo11
|
||||
name: repo11
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 12
|
||||
owner_id: 14
|
||||
lower_name: test_repo_12
|
||||
name: test_repo_12
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 13
|
||||
owner_id: 14
|
||||
lower_name: test_repo_13
|
||||
name: test_repo_13
|
||||
is_private: true
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
|
||||
-
|
||||
id: 14
|
||||
owner_id: 14
|
||||
lower_name: test_repo_14
|
||||
name: test_repo_14
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
is_mirror: false
|
||||
9
models/fixtures/star.yml
Normal file
9
models/fixtures/star.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 2
|
||||
repo_id: 2
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 2
|
||||
repo_id: 4
|
||||
35
models/fixtures/team.yml
Normal file
35
models/fixtures/team.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
-
|
||||
id: 1
|
||||
org_id: 3
|
||||
lower_name: owners
|
||||
name: Owners
|
||||
authorize: 4 # owner
|
||||
num_repos: 2
|
||||
num_members: 1
|
||||
|
||||
-
|
||||
id: 2
|
||||
org_id: 3
|
||||
lower_name: team1
|
||||
name: team1
|
||||
authorize: 2 # write
|
||||
num_repos: 1
|
||||
num_members: 2
|
||||
|
||||
-
|
||||
id: 3
|
||||
org_id: 6
|
||||
lower_name: owners
|
||||
name: Owners
|
||||
authorize: 4 # owner
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
|
||||
-
|
||||
id: 4
|
||||
org_id: 7
|
||||
lower_name: owners
|
||||
name: Owners
|
||||
authorize: 4 # owner
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
17
models/fixtures/team_repo.yml
Normal file
17
models/fixtures/team_repo.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
-
|
||||
id: 1
|
||||
org_id: 3
|
||||
team_id: 1
|
||||
repo_id: 3
|
||||
|
||||
-
|
||||
id: 2
|
||||
org_id: 3
|
||||
team_id: 2
|
||||
repo_id: 3
|
||||
|
||||
-
|
||||
id: 3
|
||||
org_id: 3
|
||||
team_id: 1
|
||||
repo_id: 5
|
||||
29
models/fixtures/team_user.yml
Normal file
29
models/fixtures/team_user.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
-
|
||||
id: 1
|
||||
org_id: 3
|
||||
team_id: 1
|
||||
uid: 2
|
||||
|
||||
-
|
||||
id: 2
|
||||
org_id: 3
|
||||
team_id: 2
|
||||
uid: 2
|
||||
|
||||
-
|
||||
id: 3
|
||||
org_id: 3
|
||||
team_id: 2
|
||||
uid: 4
|
||||
|
||||
-
|
||||
id: 4
|
||||
org_id: 6
|
||||
team_id: 3
|
||||
uid: 5
|
||||
|
||||
-
|
||||
id: 5
|
||||
org_id: 7
|
||||
team_id: 4
|
||||
uid: 5
|
||||
214
models/fixtures/user.yml
Normal file
214
models/fixtures/user.yml
Normal file
@@ -0,0 +1,214 @@
|
||||
- # NOTE: this user (id=1) is the admin
|
||||
id: 1
|
||||
lower_name: user1
|
||||
name: user1
|
||||
full_name: User One
|
||||
email: user1@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: true
|
||||
avatar: avatar1
|
||||
avatar_email: user1@example.com
|
||||
num_repos: 0
|
||||
|
||||
-
|
||||
id: 2
|
||||
lower_name: user2
|
||||
name: user2
|
||||
full_name: User Two
|
||||
email: user2@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar2
|
||||
avatar_email: user2@example.com
|
||||
num_repos: 2
|
||||
num_stars: 2
|
||||
num_followers: 1
|
||||
|
||||
-
|
||||
id: 3
|
||||
lower_name: user3
|
||||
name: user3
|
||||
full_name: User Three
|
||||
email: user3@example.com
|
||||
passwd: password
|
||||
type: 1 # organization
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar3
|
||||
avatar_email: user3@example.com
|
||||
num_repos: 2
|
||||
num_members: 2
|
||||
num_teams: 2
|
||||
|
||||
-
|
||||
id: 4
|
||||
lower_name: user4
|
||||
name: user4
|
||||
full_name: User Four
|
||||
email: user4@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar4
|
||||
avatar_email: user4@example.com
|
||||
num_repos: 0
|
||||
num_following: 1
|
||||
|
||||
-
|
||||
id: 5
|
||||
lower_name: user5
|
||||
name: user5
|
||||
full_name: User Five
|
||||
email: user5@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar5
|
||||
avatar_email: user5@example.com
|
||||
num_repos: 1
|
||||
allow_create_organization: false
|
||||
is_active: true
|
||||
num_following: 0
|
||||
|
||||
-
|
||||
id: 6
|
||||
lower_name: user6
|
||||
name: user6
|
||||
full_name: User Six
|
||||
email: user6@example.com
|
||||
passwd: password
|
||||
type: 1 # organization
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar6
|
||||
avatar_email: user6@example.com
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
num_teams: 1
|
||||
|
||||
-
|
||||
id: 7
|
||||
lower_name: user7
|
||||
name: user7
|
||||
full_name: User Seven
|
||||
email: user7@example.com
|
||||
passwd: password
|
||||
type: 1 # organization
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar7
|
||||
avatar_email: user7@example.com
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
num_teams: 1
|
||||
|
||||
-
|
||||
id: 8
|
||||
lower_name: user8
|
||||
name: user8
|
||||
full_name: User Eight
|
||||
email: user8@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar8
|
||||
avatar_email: user8@example.com
|
||||
num_repos: 0
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 9
|
||||
lower_name: user9
|
||||
name: user9
|
||||
full_name: User Nine
|
||||
email: user9@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar9
|
||||
avatar_email: user9@example.com
|
||||
num_repos: 0
|
||||
is_active: false
|
||||
|
||||
-
|
||||
id: 10
|
||||
lower_name: user10
|
||||
name: user10
|
||||
full_name: User Ten
|
||||
email: user10@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar10
|
||||
avatar_email: user10@example.com
|
||||
num_repos: 3
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 11
|
||||
lower_name: user11
|
||||
name: user11
|
||||
full_name: User Eleven
|
||||
email: user11@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar11
|
||||
avatar_email: user11@example.com
|
||||
num_repos: 1
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 12
|
||||
lower_name: user12
|
||||
name: user12
|
||||
full_name: User 12
|
||||
email: user12@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar12
|
||||
avatar_email: user12@example.com
|
||||
num_repos: 1
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 13
|
||||
lower_name: user13
|
||||
name: user13
|
||||
full_name: User 13
|
||||
email: user13@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar13
|
||||
avatar_email: user13@example.com
|
||||
num_repos: 1
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 14
|
||||
lower_name: user14
|
||||
name: user14
|
||||
full_name: User 14
|
||||
email: user14@example.com
|
||||
passwd: password
|
||||
type: 0 # individual
|
||||
salt: salt
|
||||
is_admin: false
|
||||
avatar: avatar14
|
||||
avatar_email: user13@example.com
|
||||
num_repos: 3
|
||||
is_active: true
|
||||
17
models/fixtures/user_open_id.yml
Normal file
17
models/fixtures/user_open_id.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 1
|
||||
uri: https://user1.domain1.tld/
|
||||
show: false
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 1
|
||||
uri: http://user1.domain2.tld/
|
||||
show: true
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 2
|
||||
uri: https://domain1.tld/user2/
|
||||
show: true
|
||||
9
models/fixtures/watch.yml
Normal file
9
models/fixtures/watch.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
-
|
||||
id: 1
|
||||
user_id: 1
|
||||
repo_id: 1
|
||||
|
||||
-
|
||||
id: 2
|
||||
user_id: 4
|
||||
repo_id: 1
|
||||
24
models/fixtures/webhook.yml
Normal file
24
models/fixtures/webhook.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
-
|
||||
id: 1
|
||||
repo_id: 1
|
||||
url: www.example.com/url1
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 2
|
||||
repo_id: 1
|
||||
url: www.example.com/url2
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
||||
is_active: false
|
||||
|
||||
-
|
||||
id: 3
|
||||
org_id: 3
|
||||
repo_id: 3
|
||||
url: www.example.com/url3
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
||||
is_active: true
|
||||
@@ -78,7 +78,7 @@ var (
|
||||
func diffToHTML(diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
// Reproduce signs which are cutted for inline diff before.
|
||||
// Reproduce signs which are cut for inline diff before.
|
||||
switch lineType {
|
||||
case DiffLineAdd:
|
||||
buf.WriteByte('+')
|
||||
@@ -200,6 +200,7 @@ type DiffFile struct {
|
||||
IsCreated bool
|
||||
IsDeleted bool
|
||||
IsBin bool
|
||||
IsLFSFile bool
|
||||
IsRenamed bool
|
||||
IsSubmodule bool
|
||||
Sections []*DiffSection
|
||||
@@ -233,7 +234,7 @@ const cmdDiffHead = "diff --git "
|
||||
// ParsePatch builds a Diff object from a io.Reader and some
|
||||
// parameters.
|
||||
// TODO: move this function to gogits/git-module
|
||||
func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*Diff, error) {
|
||||
func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
|
||||
var (
|
||||
diff = &Diff{Files: make([]*DiffFile, 0)}
|
||||
|
||||
@@ -245,6 +246,7 @@ func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*
|
||||
leftLine, rightLine int
|
||||
lineCount int
|
||||
curFileLinesCount int
|
||||
curFileLFSPrefix bool
|
||||
)
|
||||
|
||||
input := bufio.NewReader(reader)
|
||||
@@ -268,11 +270,33 @@ func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*
|
||||
continue
|
||||
}
|
||||
|
||||
trimLine := strings.Trim(line, "+- ")
|
||||
|
||||
if trimLine == LFSMetaFileIdentifier {
|
||||
curFileLFSPrefix = true
|
||||
}
|
||||
|
||||
if curFileLFSPrefix && strings.HasPrefix(trimLine, LFSMetaFileOidPrefix) {
|
||||
oid := strings.TrimPrefix(trimLine, LFSMetaFileOidPrefix)
|
||||
|
||||
if len(oid) == 64 {
|
||||
m := &LFSMetaObject{Oid: oid}
|
||||
count, err := x.Count(m)
|
||||
|
||||
if err == nil && count > 0 {
|
||||
curFile.IsBin = true
|
||||
curFile.IsLFSFile = true
|
||||
curSection.Lines = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curFileLinesCount++
|
||||
lineCount++
|
||||
|
||||
// Diff data too large, we only show the first about maxlines lines
|
||||
if curFileLinesCount >= maxLines || len(line) >= maxLineCharacteres {
|
||||
// Diff data too large, we only show the first about maxLines lines
|
||||
if curFileLinesCount >= maxLines || len(line) >= maxLineCharacters {
|
||||
curFile.IsIncomplete = true
|
||||
}
|
||||
|
||||
@@ -354,6 +378,7 @@ func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*
|
||||
break
|
||||
}
|
||||
curFileLinesCount = 0
|
||||
curFileLFSPrefix = false
|
||||
|
||||
// Check file diff type and is submodule.
|
||||
for {
|
||||
@@ -422,7 +447,7 @@ func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*
|
||||
// GetDiffRange builds a Diff between two commits of a repository.
|
||||
// passing the empty string as beforeCommitID returns a diff from the
|
||||
// parent commit.
|
||||
func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
|
||||
func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
|
||||
gitRepo, err := git.OpenRepository(repoPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -458,10 +483,10 @@ func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxL
|
||||
return nil, fmt.Errorf("Start: %v", err)
|
||||
}
|
||||
|
||||
pid := process.Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cmd)
|
||||
defer process.Remove(pid)
|
||||
pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cmd)
|
||||
defer process.GetManager().Remove(pid)
|
||||
|
||||
diff, err := ParsePatch(maxLines, maxLineCharacteres, maxFiles, stdout)
|
||||
diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ParsePatch: %v", err)
|
||||
}
|
||||
@@ -529,6 +554,6 @@ func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Write
|
||||
}
|
||||
|
||||
// GetDiffCommit builds a Diff representing the given commitID.
|
||||
func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
|
||||
return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacteres, maxFiles)
|
||||
func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
|
||||
return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles)
|
||||
}
|
||||
|
||||
@@ -20,16 +20,16 @@ func assertLineEqual(t *testing.T, d1 *DiffLine, d2 *DiffLine) {
|
||||
|
||||
func TestDiffToHTML(t *testing.T) {
|
||||
assertEqual(t, "+foo <span class=\"added-code\">bar</span> biz", diffToHTML([]dmp.Diff{
|
||||
dmp.Diff{dmp.DiffEqual, "foo "},
|
||||
dmp.Diff{dmp.DiffInsert, "bar"},
|
||||
dmp.Diff{dmp.DiffDelete, " baz"},
|
||||
dmp.Diff{dmp.DiffEqual, " biz"},
|
||||
{dmp.DiffEqual, "foo "},
|
||||
{dmp.DiffInsert, "bar"},
|
||||
{dmp.DiffDelete, " baz"},
|
||||
{dmp.DiffEqual, " biz"},
|
||||
}, DiffLineAdd))
|
||||
|
||||
assertEqual(t, "-foo <span class=\"removed-code\">bar</span> biz", diffToHTML([]dmp.Diff{
|
||||
dmp.Diff{dmp.DiffEqual, "foo "},
|
||||
dmp.Diff{dmp.DiffDelete, "bar"},
|
||||
dmp.Diff{dmp.DiffInsert, " baz"},
|
||||
dmp.Diff{dmp.DiffEqual, " biz"},
|
||||
{dmp.DiffEqual, "foo "},
|
||||
{dmp.DiffDelete, "bar"},
|
||||
{dmp.DiffInsert, " baz"},
|
||||
{dmp.DiffEqual, " biz"},
|
||||
}, DiffLineDel))
|
||||
}
|
||||
|
||||
462
models/gpg_key.go
Normal file
462
models/gpg_key.go
Normal file
@@ -0,0 +1,462 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/armor"
|
||||
"golang.org/x/crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
// GPGKey represents a GPG key.
|
||||
type GPGKey struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
KeyID string `xorm:"INDEX CHAR(16) NOT NULL"`
|
||||
PrimaryKeyID string `xorm:"CHAR(16)"`
|
||||
Content string `xorm:"TEXT NOT NULL"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
Expired time.Time `xorm:"-"`
|
||||
ExpiredUnix int64
|
||||
Added time.Time `xorm:"-"`
|
||||
AddedUnix int64
|
||||
SubsKey []*GPGKey `xorm:"-"`
|
||||
Emails []*EmailAddress
|
||||
CanSign bool
|
||||
CanEncryptComms bool
|
||||
CanEncryptStorage bool
|
||||
CanCertify bool
|
||||
}
|
||||
|
||||
// BeforeInsert will be invoked by XORM before inserting a record
|
||||
func (key *GPGKey) BeforeInsert() {
|
||||
key.AddedUnix = time.Now().Unix()
|
||||
key.ExpiredUnix = key.Expired.Unix()
|
||||
key.CreatedUnix = key.Created.Unix()
|
||||
}
|
||||
|
||||
// AfterSet is invoked from XORM after setting the value of a field of this object.
|
||||
func (key *GPGKey) AfterSet(colName string, _ xorm.Cell) {
|
||||
switch colName {
|
||||
case "key_id":
|
||||
x.Where("primary_key_id=?", key.KeyID).Find(&key.SubsKey)
|
||||
case "added_unix":
|
||||
key.Added = time.Unix(key.AddedUnix, 0).Local()
|
||||
case "expired_unix":
|
||||
key.Expired = time.Unix(key.ExpiredUnix, 0).Local()
|
||||
case "created_unix":
|
||||
key.Created = time.Unix(key.CreatedUnix, 0).Local()
|
||||
}
|
||||
}
|
||||
|
||||
// ListGPGKeys returns a list of public keys belongs to given user.
|
||||
func ListGPGKeys(uid int64) ([]*GPGKey, error) {
|
||||
keys := make([]*GPGKey, 0, 5)
|
||||
return keys, x.Where("owner_id=? AND primary_key_id=''", uid).Find(&keys)
|
||||
}
|
||||
|
||||
// GetGPGKeyByID returns public key by given ID.
|
||||
func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
|
||||
key := new(GPGKey)
|
||||
has, err := x.Id(keyID).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrGPGKeyNotExist{keyID}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key.
|
||||
// The function returns the actual public key on success
|
||||
func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) {
|
||||
list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list[0], nil
|
||||
}
|
||||
|
||||
//addGPGKey add key and subkeys to database
|
||||
func addGPGKey(e Engine, key *GPGKey) (err error) {
|
||||
// Save GPG primary key.
|
||||
if _, err = e.Insert(key); err != nil {
|
||||
return err
|
||||
}
|
||||
// Save GPG subs key.
|
||||
for _, subkey := range key.SubsKey {
|
||||
if err := addGPGKey(e, subkey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddGPGKey adds new public key to database.
|
||||
func AddGPGKey(ownerID int64, content string) (*GPGKey, error) {
|
||||
ekey, err := checkArmoredGPGKeyString(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Key ID cannot be duplicated.
|
||||
has, err := x.Where("key_id=?", ekey.PrimaryKey.KeyIdString()).
|
||||
Get(new(GPGKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, ErrGPGKeyIDAlreadyUsed{ekey.PrimaryKey.KeyIdString()}
|
||||
}
|
||||
|
||||
//Get DB session
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := parseGPGKey(ownerID, ekey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = addGPGKey(sess, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, sess.Commit()
|
||||
}
|
||||
|
||||
//base64EncPubKey encode public kay content to base 64
|
||||
func base64EncPubKey(pubkey *packet.PublicKey) (string, error) {
|
||||
var w bytes.Buffer
|
||||
err := pubkey.Serialize(&w)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(w.Bytes()), nil
|
||||
}
|
||||
|
||||
//parseSubGPGKey parse a sub Key
|
||||
func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) {
|
||||
content, err := base64EncPubKey(pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GPGKey{
|
||||
OwnerID: ownerID,
|
||||
KeyID: pubkey.KeyIdString(),
|
||||
PrimaryKeyID: primaryID,
|
||||
Content: content,
|
||||
Created: pubkey.CreationTime,
|
||||
Expired: expiry,
|
||||
CanSign: pubkey.CanSign(),
|
||||
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||
CanCertify: pubkey.PubKeyAlgo.CanSign(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
//parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature)
|
||||
func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
|
||||
pubkey := e.PrimaryKey
|
||||
|
||||
//Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165
|
||||
var selfSig *packet.Signature
|
||||
for _, ident := range e.Identities {
|
||||
if selfSig == nil {
|
||||
selfSig = ident.SelfSignature
|
||||
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
|
||||
selfSig = ident.SelfSignature
|
||||
break
|
||||
}
|
||||
}
|
||||
expiry := time.Time{}
|
||||
if selfSig.KeyLifetimeSecs != nil {
|
||||
expiry = selfSig.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
|
||||
}
|
||||
|
||||
//Parse Subkeys
|
||||
subkeys := make([]*GPGKey, len(e.Subkeys))
|
||||
for i, k := range e.Subkeys {
|
||||
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subkeys[i] = subs
|
||||
}
|
||||
|
||||
//Check emails
|
||||
userEmails, err := GetEmailAddresses(ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
emails := make([]*EmailAddress, len(e.Identities))
|
||||
n := 0
|
||||
for _, ident := range e.Identities {
|
||||
|
||||
for _, e := range userEmails {
|
||||
if e.Email == ident.UserId.Email && e.IsActivated {
|
||||
emails[n] = e
|
||||
break
|
||||
}
|
||||
}
|
||||
if emails[n] == nil {
|
||||
return nil, fmt.Errorf("Failed to found email or is not confirmed : %s", ident.UserId.Email)
|
||||
}
|
||||
n++
|
||||
}
|
||||
content, err := base64EncPubKey(pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GPGKey{
|
||||
OwnerID: ownerID,
|
||||
KeyID: pubkey.KeyIdString(),
|
||||
PrimaryKeyID: "",
|
||||
Content: content,
|
||||
Created: pubkey.CreationTime,
|
||||
Expired: expiry,
|
||||
Emails: emails,
|
||||
SubsKey: subkeys,
|
||||
CanSign: pubkey.CanSign(),
|
||||
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||
CanCertify: pubkey.PubKeyAlgo.CanSign(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deleteGPGKey does the actual key deletion
|
||||
func deleteGPGKey(e *xorm.Session, keyID string) (int64, error) {
|
||||
if keyID == "" {
|
||||
return 0, fmt.Errorf("empty KeyId forbidden") //Should never happen but just to be sure
|
||||
}
|
||||
return e.Where("key_id=?", keyID).Or("primary_key_id=?", keyID).Delete(new(GPGKey))
|
||||
}
|
||||
|
||||
// DeleteGPGKey deletes GPG key information in database.
|
||||
func DeleteGPGKey(doer *User, id int64) (err error) {
|
||||
key, err := GetGPGKeyByID(id)
|
||||
if err != nil {
|
||||
if IsErrGPGKeyNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("GetPublicKeyByID: %v", err)
|
||||
}
|
||||
|
||||
// Check if user has access to delete this key.
|
||||
if !doer.IsAdmin && doer.ID != key.OwnerID {
|
||||
return ErrGPGKeyAccessDenied{doer.ID, key.ID}
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = deleteGPGKey(sess, key.KeyID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = sess.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitVerification represents a commit validation of signature
|
||||
type CommitVerification struct {
|
||||
Verified bool
|
||||
Reason string
|
||||
SigningUser *User
|
||||
SigningKey *GPGKey
|
||||
}
|
||||
|
||||
// SignCommit represents a commit with validation of signature.
|
||||
type SignCommit struct {
|
||||
Verification *CommitVerification
|
||||
*UserCommit
|
||||
}
|
||||
|
||||
func readerFromBase64(s string) (io.Reader, error) {
|
||||
bs, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewBuffer(bs), nil
|
||||
}
|
||||
|
||||
func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) {
|
||||
h := hashFunc.New()
|
||||
if _, err := h.Write(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// readArmoredSign read an armored signature block with the given type. https://sourcegraph.com/github.com/golang/crypto/-/blob/openpgp/read.go#L24:6-24:17
|
||||
func readArmoredSign(r io.Reader) (body io.Reader, err error) {
|
||||
block, err := armor.Decode(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if block.Type != openpgp.SignatureType {
|
||||
return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type)
|
||||
}
|
||||
return block.Body, nil
|
||||
}
|
||||
|
||||
func extractSignature(s string) (*packet.Signature, error) {
|
||||
r, err := readArmoredSign(strings.NewReader(s))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read signature armor")
|
||||
}
|
||||
p, err := packet.Read(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read signature packet")
|
||||
}
|
||||
sig, ok := p.(*packet.Signature)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Packet is not a signature")
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
|
||||
//Check if key can sign
|
||||
if !k.CanSign {
|
||||
return fmt.Errorf("key can not sign")
|
||||
}
|
||||
//Decode key
|
||||
b, err := readerFromBase64(k.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//Read key
|
||||
p, err := packet.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Check type
|
||||
pkey, ok := p.(*packet.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("key is not a public key")
|
||||
}
|
||||
|
||||
return pkey.VerifySignature(h, s)
|
||||
}
|
||||
|
||||
// ParseCommitWithSignature check if signature is good against keystore.
|
||||
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
|
||||
|
||||
if c.Signature != nil {
|
||||
|
||||
//Parsing signature
|
||||
sig, err := extractSignature(c.Signature.Signature)
|
||||
if err != nil { //Skipping failed to extract sign
|
||||
log.Error(3, "SignatureRead err: %v", err)
|
||||
return &CommitVerification{
|
||||
Verified: false,
|
||||
Reason: "gpg.error.extract_sign",
|
||||
}
|
||||
}
|
||||
|
||||
//Find Committer account
|
||||
committer, err := GetUserByEmail(c.Committer.Email)
|
||||
if err != nil { //Skipping not user for commiter
|
||||
log.Error(3, "NoCommitterAccount: %v", err)
|
||||
return &CommitVerification{
|
||||
Verified: false,
|
||||
Reason: "gpg.error.no_committer_account",
|
||||
}
|
||||
}
|
||||
|
||||
keys, err := ListGPGKeys(committer.ID)
|
||||
if err != nil || len(keys) == 0 { //Skipping failed to get gpg keys of user
|
||||
log.Error(3, "ListGPGKeys: %v", err)
|
||||
return &CommitVerification{
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
|
||||
//Generating hash of commit
|
||||
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
|
||||
if err != nil { //Skipping ailed to generate hash
|
||||
log.Error(3, "PopulateHash: %v", err)
|
||||
return &CommitVerification{
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
//We get PK
|
||||
if err := verifySign(sig, hash, k); err == nil {
|
||||
return &CommitVerification{ //Everything is ok
|
||||
Verified: true,
|
||||
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID),
|
||||
SigningUser: committer,
|
||||
SigningKey: k,
|
||||
}
|
||||
}
|
||||
//And test also SubsKey
|
||||
for _, sk := range k.SubsKey {
|
||||
if err := verifySign(sig, hash, sk); err == nil {
|
||||
return &CommitVerification{ //Everything is ok
|
||||
Verified: true,
|
||||
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
|
||||
SigningUser: committer,
|
||||
SigningKey: sk,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &CommitVerification{ //Default at this stage
|
||||
Verified: false,
|
||||
Reason: "gpg.error.no_gpg_keys_found",
|
||||
}
|
||||
}
|
||||
|
||||
return &CommitVerification{
|
||||
Verified: false, //Default value
|
||||
Reason: "gpg.error.not_signed_commit", //Default value
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
|
||||
func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
|
||||
var (
|
||||
newCommits = list.New()
|
||||
e = oldCommits.Front()
|
||||
)
|
||||
for e != nil {
|
||||
c := e.Value.(UserCommit)
|
||||
newCommits.PushBack(SignCommit{
|
||||
UserCommit: &c,
|
||||
Verification: ParseCommitWithSignature(c.Commit),
|
||||
})
|
||||
e = e.Next()
|
||||
}
|
||||
return newCommits
|
||||
}
|
||||
164
models/gpg_key_test.go
Normal file
164
models/gpg_key_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckArmoredGPGKeyString(t *testing.T) {
|
||||
testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv
|
||||
z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m
|
||||
/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1
|
||||
vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN
|
||||
0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac
|
||||
mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE
|
||||
IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF
|
||||
Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY
|
||||
KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa
|
||||
MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ
|
||||
ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+
|
||||
sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo
|
||||
T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i
|
||||
iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE
|
||||
QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT
|
||||
pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU
|
||||
JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN
|
||||
/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx
|
||||
ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02
|
||||
cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF
|
||||
CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH
|
||||
6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk
|
||||
lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo
|
||||
RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP
|
||||
Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
|
||||
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
|
||||
=i9b7
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
key, err := checkArmoredGPGKeyString(testGPGArmor)
|
||||
assert.Nil(t, err, "Could not parse a valid GPG armored key", key)
|
||||
//TODO verify value of key
|
||||
}
|
||||
|
||||
func TestExtractSignature(t *testing.T) {
|
||||
testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv
|
||||
z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m
|
||||
/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1
|
||||
vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN
|
||||
0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac
|
||||
mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE
|
||||
IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF
|
||||
Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY
|
||||
KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa
|
||||
MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ
|
||||
ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+
|
||||
sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo
|
||||
T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i
|
||||
iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE
|
||||
QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT
|
||||
pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU
|
||||
JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN
|
||||
/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx
|
||||
ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02
|
||||
cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF
|
||||
CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH
|
||||
6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk
|
||||
lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo
|
||||
RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP
|
||||
Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
|
||||
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
|
||||
=i9b7
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
ekey, err := checkArmoredGPGKeyString(testGPGArmor)
|
||||
assert.Nil(t, err, "Could not parse a valid GPG armored key", ekey)
|
||||
|
||||
pubkey := ekey.PrimaryKey
|
||||
content, err := base64EncPubKey(pubkey)
|
||||
assert.Nil(t, err, "Could not base64 encode a valid PublicKey content", ekey)
|
||||
|
||||
key := &GPGKey{
|
||||
KeyID: pubkey.KeyIdString(),
|
||||
Content: content,
|
||||
Created: pubkey.CreationTime,
|
||||
CanSign: pubkey.CanSign(),
|
||||
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||
CanCertify: pubkey.PubKeyAlgo.CanSign(),
|
||||
}
|
||||
|
||||
cannotsignkey := &GPGKey{
|
||||
KeyID: pubkey.KeyIdString(),
|
||||
Content: content,
|
||||
Created: pubkey.CreationTime,
|
||||
CanSign: false,
|
||||
CanEncryptComms: false,
|
||||
CanEncryptStorage: false,
|
||||
CanCertify: false,
|
||||
}
|
||||
|
||||
testGoodSigArmor := `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAABCAAdFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAljAiQIACgkQroJVuKDY
|
||||
KORvCgf6A/Ehh0r7QbO2tFEghT+/Ab+bN7jRN3zP9ed6/q/ophYmkrU0NibtbJH9
|
||||
AwFVdHxCmj78SdiRjaTKyevklXw34nvMftmvnOI4lBNUdw6KWl25/n/7wN0l2oZW
|
||||
rW3UawYpZgodXiLTYarfEimkDQmT67ArScjRA6lLbkEYKO0VdwDu+Z6yBUH3GWtm
|
||||
45RkXpnsF6AXUfuD7YxnfyyDE1A7g7zj4vVYUAfWukJjqow/LsCUgETETJOqj9q3
|
||||
52/oQDs04fVkIEtCDulcY+K/fKlukBPJf9WceNDEqiENUzN/Z1y0E+tJ07cSy4bk
|
||||
yIJb+d0OAaG8bxloO7nJq4Res1Qa8Q==
|
||||
=puvG
|
||||
-----END PGP SIGNATURE-----`
|
||||
testGoodPayload := `tree 56ae8d2799882b20381fc11659db06c16c68c61a
|
||||
parent c7870c39e4e6b247235ca005797703ec4254613f
|
||||
author Antoine GIRARD <sapk@sapk.fr> 1489012989 +0100
|
||||
committer Antoine GIRARD <sapk@sapk.fr> 1489012989 +0100
|
||||
|
||||
Goog GPG
|
||||
`
|
||||
|
||||
testBadSigArmor := `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAABCAAdFiEE5yr4rn9ulbdMxJFiPYI/ySNrtNkFAljAiYkACgkQPYI/ySNr
|
||||
tNmDdQf+NXhVRiOGt0GucpjJCGrOnK/qqVUmQyRUfrqzVUdb/1/Ws84V5/wE547I
|
||||
6z3oxeBKFsJa1CtIlxYaUyVhYnDzQtphJzub+Aw3UG0E2ywiE+N7RCa1Ufl7pPxJ
|
||||
U0SD6gvNaeTDQV/Wctu8v8DkCtEd3N8cMCDWhvy/FQEDztVtzm8hMe0Vdm0ozEH6
|
||||
P0W93sDNkLC5/qpWDN44sFlYDstW5VhMrnF0r/ohfaK2kpYHhkPk7WtOoHSUwQSg
|
||||
c4gfhjvXIQrWFnII1Kr5jFGlmgNSR02qpb31VGkMzSnBhWVf2OaHS/kI49QHJakq
|
||||
AhVDEnoYLCgoDGg9c3p1Ll2452/c6Q==
|
||||
=uoGV
|
||||
-----END PGP SIGNATURE-----`
|
||||
testBadPayload := `tree 3074ff04951956a974e8b02d57733b0766f7cf6c
|
||||
parent fd3577542f7ad1554c7c7c0eb86bb57a1324ad91
|
||||
author Antoine GIRARD <sapk@sapk.fr> 1489013107 +0100
|
||||
committer Antoine GIRARD <sapk@sapk.fr> 1489013107 +0100
|
||||
|
||||
Unkonwn GPG key with good email
|
||||
`
|
||||
//Reading Sign
|
||||
goodSig, err := extractSignature(testGoodSigArmor)
|
||||
assert.Nil(t, err, "Could not parse a valid GPG armored signature", testGoodSigArmor)
|
||||
badSig, err := extractSignature(testBadSigArmor)
|
||||
assert.Nil(t, err, "Could not parse a valid GPG armored signature", testBadSigArmor)
|
||||
|
||||
//Generating hash of commit
|
||||
goodHash, err := populateHash(goodSig.Hash, []byte(testGoodPayload))
|
||||
assert.Nil(t, err, "Could not generate a valid hash of payload", testGoodPayload)
|
||||
badHash, err := populateHash(badSig.Hash, []byte(testBadPayload))
|
||||
assert.Nil(t, err, "Could not generate a valid hash of payload", testBadPayload)
|
||||
|
||||
//Verify
|
||||
err = verifySign(goodSig, goodHash, key)
|
||||
assert.Nil(t, err, "Could not validate a good signature")
|
||||
err = verifySign(badSig, badHash, key)
|
||||
assert.NotNil(t, err, "Validate a bad signature")
|
||||
err = verifySign(goodSig, goodHash, cannotsignkey)
|
||||
assert.NotNil(t, err, "Validate a bad signature with a kay that can not sign")
|
||||
}
|
||||
108
models/graph.go
Normal file
108
models/graph.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright 2016 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/git"
|
||||
)
|
||||
|
||||
// GraphItem represent one commit, or one relation in timeline
|
||||
type GraphItem struct {
|
||||
GraphAcii string
|
||||
Relation string
|
||||
Branch string
|
||||
Rev string
|
||||
Date string
|
||||
Author string
|
||||
AuthorEmail string
|
||||
ShortRev string
|
||||
Subject string
|
||||
OnlyRelation bool
|
||||
}
|
||||
|
||||
// GraphItems is a list of commits from all branches
|
||||
type GraphItems []GraphItem
|
||||
|
||||
// GetCommitGraph return a list of commit (GraphItems) from all branches
|
||||
func GetCommitGraph(r *git.Repository) (GraphItems, error) {
|
||||
|
||||
var CommitGraph []GraphItem
|
||||
|
||||
format := "DATA:|%d|%H|%ad|%an|%ae|%h|%s"
|
||||
|
||||
graphCmd := git.NewCommand("log")
|
||||
graphCmd.AddArguments("--graph",
|
||||
"--date-order",
|
||||
"--all",
|
||||
"-C",
|
||||
"-M",
|
||||
"-n 100",
|
||||
"--date=iso",
|
||||
fmt.Sprintf("--pretty=format:%s", format),
|
||||
)
|
||||
graph, err := graphCmd.RunInDir(r.Path)
|
||||
if err != nil {
|
||||
return CommitGraph, err
|
||||
}
|
||||
|
||||
CommitGraph = make([]GraphItem, 0, 100)
|
||||
for _, s := range strings.Split(graph, "\n") {
|
||||
GraphItem, err := graphItemFromString(s, r)
|
||||
if err != nil {
|
||||
return CommitGraph, err
|
||||
}
|
||||
CommitGraph = append(CommitGraph, GraphItem)
|
||||
}
|
||||
|
||||
return CommitGraph, nil
|
||||
}
|
||||
|
||||
func graphItemFromString(s string, r *git.Repository) (GraphItem, error) {
|
||||
|
||||
var ascii string
|
||||
var data = "|||||||"
|
||||
lines := strings.Split(s, "DATA:")
|
||||
|
||||
switch len(lines) {
|
||||
case 1:
|
||||
ascii = lines[0]
|
||||
case 2:
|
||||
ascii = lines[0]
|
||||
data = lines[1]
|
||||
default:
|
||||
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s. Expect 1 or two fields", s)
|
||||
}
|
||||
|
||||
rows := strings.SplitN(data, "|", 8)
|
||||
if len(rows) < 8 {
|
||||
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s - Should containt 8 datafields", s)
|
||||
}
|
||||
|
||||
/* // see format in getCommitGraph()
|
||||
0 Relation string
|
||||
1 Branch string
|
||||
2 Rev string
|
||||
3 Date string
|
||||
4 Author string
|
||||
5 AuthorEmail string
|
||||
6 ShortRev string
|
||||
7 Subject string
|
||||
*/
|
||||
gi := GraphItem{ascii,
|
||||
rows[0],
|
||||
rows[1],
|
||||
rows[2],
|
||||
rows[3],
|
||||
rows[4],
|
||||
rows[5],
|
||||
rows[6],
|
||||
rows[7],
|
||||
len(rows[2]) == 0, // no commits referred to, only relation in current line.
|
||||
}
|
||||
return gi, nil
|
||||
}
|
||||
45
models/graph_test.go
Normal file
45
models/graph_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2016 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/git"
|
||||
)
|
||||
|
||||
func BenchmarkGetCommitGraph(b *testing.B) {
|
||||
|
||||
currentRepo, err := git.OpenRepository(".")
|
||||
if err != nil {
|
||||
b.Error("Could not open repository")
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
graph, err := GetCommitGraph(currentRepo)
|
||||
if err != nil {
|
||||
b.Error("Could get commit graph")
|
||||
}
|
||||
|
||||
if len(graph) < 100 {
|
||||
b.Error("Should get 100 log lines.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseCommitString(b *testing.B) {
|
||||
testString := "* DATA:||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Kjell Kvinge|kjell@kvinge.biz|4e61bac|Add route for graph"
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
graphItem, err := graphItemFromString(testString, nil)
|
||||
if err != nil {
|
||||
b.Error("could not parse teststring")
|
||||
}
|
||||
|
||||
if graphItem.Author != "Kjell Kvinge" {
|
||||
b.Error("Did not get expected data")
|
||||
}
|
||||
}
|
||||
}
|
||||
21
models/helper.go
Normal file
21
models/helper.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
func keysInt64(m map[int64]struct{}) []int64 {
|
||||
var keys = make([]int64, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func valuesRepository(m map[int64]*Repository) []*Repository {
|
||||
var values = make([]*Repository, 0, len(m))
|
||||
for _, v := range m {
|
||||
values = append(values, v)
|
||||
}
|
||||
return values
|
||||
}
|
||||
1283
models/issue.go
1283
models/issue.go
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,16 @@ const (
|
||||
CommentTypeCommentRef
|
||||
// Reference from a pull request
|
||||
CommentTypePullRef
|
||||
// Labels changed
|
||||
CommentTypeLabel
|
||||
// Milestone changed
|
||||
CommentTypeMilestone
|
||||
// Assignees changed
|
||||
CommentTypeAssignees
|
||||
// Change Title
|
||||
CommentTypeChangeTitle
|
||||
// Delete Branch
|
||||
CommentTypeDeleteBranch
|
||||
)
|
||||
|
||||
// CommentTag defines comment tag type
|
||||
@@ -51,20 +61,33 @@ const (
|
||||
|
||||
// Comment represents a comment in commit and issue page.
|
||||
type Comment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type CommentType
|
||||
PosterID int64
|
||||
Poster *User `xorm:"-"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type CommentType
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *User `xorm:"-"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
LabelID int64
|
||||
Label *Label `xorm:"-"`
|
||||
OldMilestoneID int64
|
||||
MilestoneID int64
|
||||
OldMilestone *Milestone `xorm:"-"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
OldAssigneeID int64
|
||||
AssigneeID int64
|
||||
Assignee *User `xorm:"-"`
|
||||
OldAssignee *User `xorm:"-"`
|
||||
OldTitle string
|
||||
NewTitle string
|
||||
|
||||
CommitID int64
|
||||
Line int64
|
||||
Content string `xorm:"TEXT"`
|
||||
RenderedContent string `xorm:"-"`
|
||||
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
CreatedUnix int64 `xorm:"INDEX"`
|
||||
Updated time.Time `xorm:"-"`
|
||||
UpdatedUnix int64
|
||||
UpdatedUnix int64 `xorm:"INDEX"`
|
||||
|
||||
// Reference issue in commit message
|
||||
CommitSHA string `xorm:"VARCHAR(40)"`
|
||||
@@ -185,6 +208,71 @@ func (c *Comment) EventTag() string {
|
||||
return "event-" + com.ToStr(c.ID)
|
||||
}
|
||||
|
||||
// LoadLabel if comment.Type is CommentTypeLabel, then load Label
|
||||
func (c *Comment) LoadLabel() error {
|
||||
var label Label
|
||||
has, err := x.ID(c.LabelID).Get(&label)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
c.Label = &label
|
||||
} else {
|
||||
// Ignore Label is deleted, but not clear this table
|
||||
log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
|
||||
func (c *Comment) LoadMilestone() error {
|
||||
if c.OldMilestoneID > 0 {
|
||||
var oldMilestone Milestone
|
||||
has, err := x.ID(c.OldMilestoneID).Get(&oldMilestone)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return ErrMilestoneNotExist{
|
||||
ID: c.OldMilestoneID,
|
||||
}
|
||||
}
|
||||
c.OldMilestone = &oldMilestone
|
||||
}
|
||||
|
||||
if c.MilestoneID > 0 {
|
||||
var milestone Milestone
|
||||
has, err := x.ID(c.MilestoneID).Get(&milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return ErrMilestoneNotExist{
|
||||
ID: c.MilestoneID,
|
||||
}
|
||||
}
|
||||
c.Milestone = &milestone
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAssignees if comment.Type is CommentTypeAssignees, then load assignees
|
||||
func (c *Comment) LoadAssignees() error {
|
||||
var err error
|
||||
if c.OldAssigneeID > 0 {
|
||||
c.OldAssignee, err = getUserByID(x, c.OldAssigneeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.AssigneeID > 0 {
|
||||
c.Assignee, err = getUserByID(x, c.AssigneeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MailParticipants sends new comment emails to repository watchers
|
||||
// and mentioned people.
|
||||
func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
|
||||
@@ -209,20 +297,35 @@ func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (e
|
||||
}
|
||||
|
||||
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
|
||||
var LabelID int64
|
||||
if opts.Label != nil {
|
||||
LabelID = opts.Label.ID
|
||||
}
|
||||
comment := &Comment{
|
||||
Type: opts.Type,
|
||||
PosterID: opts.Doer.ID,
|
||||
Poster: opts.Doer,
|
||||
IssueID: opts.Issue.ID,
|
||||
CommitID: opts.CommitID,
|
||||
CommitSHA: opts.CommitSHA,
|
||||
Line: opts.LineNum,
|
||||
Content: opts.Content,
|
||||
Type: opts.Type,
|
||||
PosterID: opts.Doer.ID,
|
||||
Poster: opts.Doer,
|
||||
IssueID: opts.Issue.ID,
|
||||
LabelID: LabelID,
|
||||
OldMilestoneID: opts.OldMilestoneID,
|
||||
MilestoneID: opts.MilestoneID,
|
||||
OldAssigneeID: opts.OldAssigneeID,
|
||||
AssigneeID: opts.AssigneeID,
|
||||
CommitID: opts.CommitID,
|
||||
CommitSHA: opts.CommitSHA,
|
||||
Line: opts.LineNum,
|
||||
Content: opts.Content,
|
||||
OldTitle: opts.OldTitle,
|
||||
NewTitle: opts.NewTitle,
|
||||
}
|
||||
if _, err = e.Insert(comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = opts.Repo.getOwner(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
||||
// This object will be used to notify watchers in the end of function.
|
||||
act := &Action{
|
||||
@@ -324,18 +427,83 @@ func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *I
|
||||
})
|
||||
}
|
||||
|
||||
func createLabelComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, label *Label, add bool) (*Comment, error) {
|
||||
var content string
|
||||
if add {
|
||||
content = "1"
|
||||
}
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeLabel,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
Label: label,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldMilestoneID, milestoneID int64) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeMilestone,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
OldMilestoneID: oldMilestoneID,
|
||||
MilestoneID: milestoneID,
|
||||
})
|
||||
}
|
||||
|
||||
func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldAssigneeID, assigneeID int64) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeAssignees,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
OldAssigneeID: oldAssigneeID,
|
||||
AssigneeID: assigneeID,
|
||||
})
|
||||
}
|
||||
|
||||
func createChangeTitleComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldTitle, newTitle string) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeChangeTitle,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
OldTitle: oldTitle,
|
||||
NewTitle: newTitle,
|
||||
})
|
||||
}
|
||||
|
||||
func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, branchName string) (*Comment, error) {
|
||||
return createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeDeleteBranch,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
CommitSHA: branchName,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentOptions defines options for creating comment
|
||||
type CreateCommentOptions struct {
|
||||
Type CommentType
|
||||
Doer *User
|
||||
Repo *Repository
|
||||
Issue *Issue
|
||||
Label *Label
|
||||
|
||||
CommitID int64
|
||||
CommitSHA string
|
||||
LineNum int64
|
||||
Content string
|
||||
Attachments []string // UUIDs of attachments
|
||||
OldMilestoneID int64
|
||||
MilestoneID int64
|
||||
OldAssigneeID int64
|
||||
AssigneeID int64
|
||||
OldTitle string
|
||||
NewTitle string
|
||||
CommitID int64
|
||||
CommitSHA string
|
||||
LineNum int64
|
||||
Content string
|
||||
Attachments []string // UUIDs of attachments
|
||||
}
|
||||
|
||||
// CreateComment creates comment of issue or commit.
|
||||
@@ -420,9 +588,11 @@ func getCommentsByIssueIDSince(e Engine, issueID, since int64) ([]*Comment, erro
|
||||
|
||||
func getCommentsByRepoIDSince(e Engine, repoID, since int64) ([]*Comment, error) {
|
||||
comments := make([]*Comment, 0, 10)
|
||||
sess := e.Where("issue.repo_id = ?", repoID).Join("INNER", "issue", "issue.id = comment.issue_id", repoID).Asc("created_unix")
|
||||
sess := e.Where("issue.repo_id = ?", repoID).
|
||||
Join("INNER", "issue", "issue.id = comment.issue_id").
|
||||
Asc("comment.created_unix")
|
||||
if since > 0 {
|
||||
sess.And("updated_unix >= ?", since)
|
||||
sess.And("comment.updated_unix >= ?", since)
|
||||
}
|
||||
return comments, sess.Find(&comments)
|
||||
}
|
||||
@@ -452,28 +622,22 @@ func UpdateComment(c *Comment) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteCommentByID deletes the comment by given ID.
|
||||
func DeleteCommentByID(id int64) error {
|
||||
comment, err := GetCommentByID(id)
|
||||
if err != nil {
|
||||
if IsErrCommentNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteComment deletes the comment
|
||||
func DeleteComment(comment *Comment) error {
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Id(comment.ID).Delete(new(Comment)); err != nil {
|
||||
if _, err := sess.Delete(&Comment{
|
||||
ID: comment.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if comment.Type == CommentTypeComment {
|
||||
if _, err = sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
|
||||
if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
183
models/issue_indexer.go
Normal file
183
models/issue_indexer.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/analysis/analyzer/simple"
|
||||
"github.com/blevesearch/bleve/search/query"
|
||||
)
|
||||
|
||||
// issueIndexerUpdateQueue queue of issues that need to be updated in the issues
|
||||
// indexer
|
||||
var issueIndexerUpdateQueue chan *Issue
|
||||
|
||||
// issueIndexer (thread-safe) index for searching issues
|
||||
var issueIndexer bleve.Index
|
||||
|
||||
// issueIndexerData data stored in the issue indexer
|
||||
type issueIndexerData struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
|
||||
Title string
|
||||
Content string
|
||||
}
|
||||
|
||||
// numericQuery an numeric-equality query for the given value and field
|
||||
func numericQuery(value int64, field string) *query.NumericRangeQuery {
|
||||
f := float64(value)
|
||||
tru := true
|
||||
q := bleve.NewNumericRangeInclusiveQuery(&f, &f, &tru, &tru)
|
||||
q.SetField(field)
|
||||
return q
|
||||
}
|
||||
|
||||
// SearchIssuesByKeyword searches for issues by given conditions.
|
||||
// Returns the matching issue IDs
|
||||
func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) {
|
||||
terms := strings.Fields(strings.ToLower(keyword))
|
||||
indexerQuery := bleve.NewConjunctionQuery(
|
||||
numericQuery(repoID, "RepoID"),
|
||||
bleve.NewDisjunctionQuery(
|
||||
bleve.NewPhraseQuery(terms, "Title"),
|
||||
bleve.NewPhraseQuery(terms, "Content"),
|
||||
))
|
||||
search := bleve.NewSearchRequestOptions(indexerQuery, 2147483647, 0, false)
|
||||
search.Fields = []string{"ID"}
|
||||
|
||||
result, err := issueIndexer.Search(search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueIDs := make([]int64, len(result.Hits))
|
||||
for i, hit := range result.Hits {
|
||||
issueIDs[i] = int64(hit.Fields["ID"].(float64))
|
||||
}
|
||||
return issueIDs, nil
|
||||
}
|
||||
|
||||
// InitIssueIndexer initialize issue indexer
|
||||
func InitIssueIndexer() {
|
||||
_, err := os.Stat(setting.Indexer.IssuePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = createIssueIndexer(); err != nil {
|
||||
log.Fatal(4, "CreateIssuesIndexer: %v", err)
|
||||
}
|
||||
if err = populateIssueIndexer(); err != nil {
|
||||
log.Fatal(4, "PopulateIssuesIndex: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Fatal(4, "InitIssuesIndexer: %v", err)
|
||||
}
|
||||
} else {
|
||||
issueIndexer, err = bleve.Open(setting.Indexer.IssuePath)
|
||||
if err != nil {
|
||||
log.Fatal(4, "InitIssuesIndexer, open index: %v", err)
|
||||
}
|
||||
}
|
||||
issueIndexerUpdateQueue = make(chan *Issue, setting.Indexer.UpdateQueueLength)
|
||||
go processIssueIndexerUpdateQueue()
|
||||
// TODO close issueIndexer when Gitea closes
|
||||
}
|
||||
|
||||
// createIssueIndexer create an issue indexer if one does not already exist
|
||||
func createIssueIndexer() error {
|
||||
mapping := bleve.NewIndexMapping()
|
||||
docMapping := bleve.NewDocumentMapping()
|
||||
|
||||
docMapping.AddFieldMappingsAt("ID", bleve.NewNumericFieldMapping())
|
||||
docMapping.AddFieldMappingsAt("RepoID", bleve.NewNumericFieldMapping())
|
||||
|
||||
textFieldMapping := bleve.NewTextFieldMapping()
|
||||
textFieldMapping.Analyzer = simple.Name
|
||||
docMapping.AddFieldMappingsAt("Title", textFieldMapping)
|
||||
docMapping.AddFieldMappingsAt("Content", textFieldMapping)
|
||||
|
||||
mapping.AddDocumentMapping("issues", docMapping)
|
||||
|
||||
var err error
|
||||
issueIndexer, err = bleve.New(setting.Indexer.IssuePath, mapping)
|
||||
return err
|
||||
}
|
||||
|
||||
// populateIssueIndexer populate the issue indexer with issue data
|
||||
func populateIssueIndexer() error {
|
||||
for page := 1; ; page++ {
|
||||
repos, _, err := Repositories(&SearchRepoOptions{
|
||||
Page: page,
|
||||
PageSize: 10,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Repositories: %v", err)
|
||||
}
|
||||
if len(repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
batch := issueIndexer.NewBatch()
|
||||
for _, repo := range repos {
|
||||
issues, err := Issues(&IssuesOptions{
|
||||
RepoID: repo.ID,
|
||||
IsClosed: util.OptionalBoolNone,
|
||||
IsPull: util.OptionalBoolNone,
|
||||
Page: -1, // do not page
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Issues: %v", err)
|
||||
}
|
||||
for _, issue := range issues {
|
||||
err = batch.Index(issue.indexUID(), issue.issueData())
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch.Index: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = issueIndexer.Batch(batch); err != nil {
|
||||
return fmt.Errorf("index.Batch: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processIssueIndexerUpdateQueue() {
|
||||
for {
|
||||
select {
|
||||
case issue := <-issueIndexerUpdateQueue:
|
||||
if err := issueIndexer.Index(issue.indexUID(), issue.issueData()); err != nil {
|
||||
log.Error(4, "issuesIndexer.Index: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// indexUID a unique identifier for an issue used in full-text indices
|
||||
func (issue *Issue) indexUID() string {
|
||||
return strconv.FormatInt(issue.ID, 36)
|
||||
}
|
||||
|
||||
func (issue *Issue) issueData() *issueIndexerData {
|
||||
return &issueIndexerData{
|
||||
ID: issue.ID,
|
||||
RepoID: issue.RepoID,
|
||||
Title: issue.Title,
|
||||
Content: issue.Content,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateIssueIndexer add/update an issue to the issue indexer
|
||||
func UpdateIssueIndexer(issue *Issue) {
|
||||
go func() {
|
||||
issueIndexerUpdateQueue <- issue
|
||||
}()
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, err
|
||||
Name: labelName,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := x.Get(l)
|
||||
has, err := e.Get(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -135,7 +135,7 @@ func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
|
||||
ID: labelID,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := x.Get(l)
|
||||
has, err := e.Get(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -171,32 +171,29 @@ func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
|
||||
}
|
||||
|
||||
// GetLabelsByRepoID returns all labels that belong to given repository by ID.
|
||||
func GetLabelsByRepoID(repoID int64) ([]*Label, error) {
|
||||
func GetLabelsByRepoID(repoID int64, sortType string) ([]*Label, error) {
|
||||
labels := make([]*Label, 0, 10)
|
||||
return labels, x.
|
||||
Where("repo_id = ?", repoID).
|
||||
Asc("name").
|
||||
Find(&labels)
|
||||
sess := x.Where("repo_id = ?", repoID)
|
||||
|
||||
switch sortType {
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
|
||||
return labels, sess.Find(&labels)
|
||||
}
|
||||
|
||||
func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
|
||||
issueLabels, err := getIssueLabels(e, issueID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getIssueLabels: %v", err)
|
||||
} else if len(issueLabels) == 0 {
|
||||
return []*Label{}, nil
|
||||
}
|
||||
|
||||
labelIDs := make([]int64, len(issueLabels))
|
||||
for i := range issueLabels {
|
||||
labelIDs[i] = issueLabels[i].LabelID
|
||||
}
|
||||
|
||||
labels := make([]*Label, 0, len(labelIDs))
|
||||
return labels, e.
|
||||
Where("id > 0").
|
||||
In("id", labelIDs).
|
||||
Asc("name").
|
||||
var labels []*Label
|
||||
return labels, e.Where("issue_label.issue_id = ?", issueID).
|
||||
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
|
||||
Asc("label.name").
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
@@ -239,6 +236,11 @@ func DeleteLabel(repoID, labelID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear label id in comment table
|
||||
if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Update(&Comment{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
@@ -249,7 +251,7 @@ func DeleteLabel(repoID, labelID int64) error {
|
||||
// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
|
||||
// \/ \/ \/ \/ \/ \/ \/
|
||||
|
||||
// IssueLabel represetns an issue-lable relation.
|
||||
// IssueLabel represents an issue-label relation.
|
||||
type IssueLabel struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"UNIQUE(s)"`
|
||||
@@ -266,7 +268,7 @@ func HasIssueLabel(issueID, labelID int64) bool {
|
||||
return hasIssueLabel(x, issueID, labelID)
|
||||
}
|
||||
|
||||
func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
|
||||
func newIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
|
||||
if _, err = e.Insert(&IssueLabel{
|
||||
IssueID: issue.ID,
|
||||
LabelID: label.ID,
|
||||
@@ -274,6 +276,14 @@ func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = issue.loadRepo(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = createLabelComment(e, doer, issue.Repo, issue, label, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
label.NumIssues++
|
||||
if issue.IsClosed {
|
||||
label.NumClosedIssues++
|
||||
@@ -282,7 +292,7 @@ func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
|
||||
}
|
||||
|
||||
// NewIssueLabel creates a new issue-label relation.
|
||||
func NewIssueLabel(issue *Issue, label *Label) (err error) {
|
||||
func NewIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
|
||||
if HasIssueLabel(issue.ID, label.ID) {
|
||||
return nil
|
||||
}
|
||||
@@ -293,20 +303,20 @@ func NewIssueLabel(issue *Issue, label *Label) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = newIssueLabel(sess, issue, label); err != nil {
|
||||
if err = newIssueLabel(sess, issue, label, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label) (err error) {
|
||||
func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label, doer *User) (err error) {
|
||||
for i := range labels {
|
||||
if hasIssueLabel(e, issue.ID, labels[i].ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = newIssueLabel(e, issue, labels[i]); err != nil {
|
||||
if err = newIssueLabel(e, issue, labels[i], doer); err != nil {
|
||||
return fmt.Errorf("newIssueLabel: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -315,14 +325,14 @@ func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label) (err error)
|
||||
}
|
||||
|
||||
// NewIssueLabels creates a list of issue-label relations.
|
||||
func NewIssueLabels(issue *Issue, labels []*Label) (err error) {
|
||||
func NewIssueLabels(issue *Issue, labels []*Label, doer *User) (err error) {
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = newIssueLabels(sess, issue, labels); err != nil {
|
||||
if err = newIssueLabels(sess, issue, labels, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -337,17 +347,22 @@ func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) {
|
||||
Find(&issueLabels)
|
||||
}
|
||||
|
||||
// GetIssueLabels returns all issue-label relations of given issue by ID.
|
||||
func GetIssueLabels(issueID int64) ([]*IssueLabel, error) {
|
||||
return getIssueLabels(x, issueID)
|
||||
}
|
||||
|
||||
func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
|
||||
if _, err = e.Delete(&IssueLabel{
|
||||
func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
|
||||
if count, err := e.Delete(&IssueLabel{
|
||||
IssueID: issue.ID,
|
||||
LabelID: label.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = issue.loadRepo(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = createLabelComment(e, doer, issue.Repo, issue, label, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
label.NumIssues--
|
||||
@@ -358,14 +373,14 @@ func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
|
||||
}
|
||||
|
||||
// DeleteIssueLabel deletes issue-label relation.
|
||||
func DeleteIssueLabel(issue *Issue, label *Label) (err error) {
|
||||
func DeleteIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = deleteIssueLabel(sess, issue, label); err != nil {
|
||||
if err = deleteIssueLabel(sess, issue, label, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
252
models/issue_label_test.go
Normal file
252
models/issue_label_test.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
api "code.gitea.io/sdk/gitea"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TODO TestGetLabelTemplateFile
|
||||
|
||||
func TestLabel_APIFormat(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
assert.Equal(t, api.Label{
|
||||
ID: label.ID,
|
||||
Name: label.Name,
|
||||
Color: "abcdef",
|
||||
}, *label.APIFormat())
|
||||
}
|
||||
|
||||
func TestLabel_CalOpenIssues(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
label.CalOpenIssues()
|
||||
assert.EqualValues(t, 2, label.NumOpenIssues)
|
||||
}
|
||||
|
||||
func TestLabel_ForegroundColor(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
assert.Equal(t, template.CSS("#000"), label.ForegroundColor())
|
||||
|
||||
label = AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
|
||||
assert.Equal(t, template.CSS("#fff"), label.ForegroundColor())
|
||||
}
|
||||
|
||||
func TestNewLabels(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
labels := []*Label{
|
||||
{RepoID: 2, Name: "labelName2", Color: "#123456"},
|
||||
{RepoID: 3, Name: "labelName3", Color: "#234567"},
|
||||
}
|
||||
for _, label := range labels {
|
||||
AssertNotExistsBean(t, label)
|
||||
}
|
||||
assert.NoError(t, NewLabels(labels...))
|
||||
for _, label := range labels {
|
||||
AssertExistsAndLoadBean(t, label)
|
||||
}
|
||||
CheckConsistencyFor(t, &Label{}, &Repository{})
|
||||
}
|
||||
|
||||
func TestGetLabelByID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label, err := GetLabelByID(1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, label.ID)
|
||||
|
||||
_, err = GetLabelByID(NonexistentID)
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelInRepoByName(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label, err := GetLabelInRepoByName(1, "label1")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, label.ID)
|
||||
assert.Equal(t, "label1", label.Name)
|
||||
|
||||
_, err = GetLabelInRepoByName(1, "")
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInRepoByName(NonexistentID, "nonexistent")
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelInRepoByID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label, err := GetLabelInRepoByID(1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, label.ID)
|
||||
|
||||
_, err = GetLabelInRepoByID(1, -1)
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
|
||||
_, err = GetLabelInRepoByID(NonexistentID, NonexistentID)
|
||||
assert.True(t, IsErrLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelsInRepoByIDs(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
labels, err := GetLabelsInRepoByIDs(1, []int64{1, 2, NonexistentID})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, 2)
|
||||
assert.EqualValues(t, 1, labels[0].ID)
|
||||
assert.EqualValues(t, 2, labels[1].ID)
|
||||
}
|
||||
|
||||
func TestGetLabelsByRepoID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := GetLabelsByRepoID(repoID, sortType)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, len(expectedIssueIDs))
|
||||
for i, label := range labels {
|
||||
assert.EqualValues(t, expectedIssueIDs[i], label.ID)
|
||||
}
|
||||
}
|
||||
testSuccess(1, "leastissues", []int64{2, 1})
|
||||
testSuccess(1, "mostissues", []int64{1, 2})
|
||||
testSuccess(1, "reversealphabetically", []int64{2, 1})
|
||||
testSuccess(1, "default", []int64{1, 2})
|
||||
}
|
||||
|
||||
func TestGetLabelsByIssueID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
labels, err := GetLabelsByIssueID(1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, 1)
|
||||
assert.EqualValues(t, 1, labels[0].ID)
|
||||
|
||||
labels, err = GetLabelsByIssueID(NonexistentID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, 0)
|
||||
}
|
||||
|
||||
func TestUpdateLabel(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
label.Color = "#ffff00"
|
||||
label.Name = "newLabelName"
|
||||
assert.NoError(t, UpdateLabel(label))
|
||||
newLabel := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
assert.Equal(t, *label, *newLabel)
|
||||
CheckConsistencyFor(t, &Label{}, &Repository{})
|
||||
}
|
||||
|
||||
func TestDeleteLabel(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
assert.NoError(t, DeleteLabel(label.RepoID, label.ID))
|
||||
AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID})
|
||||
|
||||
assert.NoError(t, DeleteLabel(label.RepoID, label.ID))
|
||||
AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID})
|
||||
|
||||
assert.NoError(t, DeleteLabel(NonexistentID, NonexistentID))
|
||||
CheckConsistencyFor(t, &Label{}, &Repository{})
|
||||
}
|
||||
|
||||
func TestHasIssueLabel(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
assert.True(t, HasIssueLabel(1, 1))
|
||||
assert.False(t, HasIssueLabel(1, 2))
|
||||
assert.False(t, HasIssueLabel(NonexistentID, NonexistentID))
|
||||
}
|
||||
|
||||
func TestNewIssueLabel(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
|
||||
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
// add new IssueLabel
|
||||
prevNumIssues := label.NumIssues
|
||||
assert.NoError(t, NewIssueLabel(issue, label, doer))
|
||||
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label.ID})
|
||||
AssertExistsAndLoadBean(t, &Comment{
|
||||
Type: CommentTypeLabel,
|
||||
PosterID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
LabelID: label.ID,
|
||||
Content: "1",
|
||||
})
|
||||
assert.EqualValues(t, prevNumIssues+1, label.NumIssues)
|
||||
|
||||
// re-add existing IssueLabel
|
||||
assert.NoError(t, NewIssueLabel(issue, label, doer))
|
||||
CheckConsistencyFor(t, &Issue{}, &Label{})
|
||||
}
|
||||
|
||||
func TestNewIssueLabels(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
label1 := AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
label2 := AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 5}).(*Issue)
|
||||
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
assert.NoError(t, NewIssueLabels(issue, []*Label{label1, label2}, doer))
|
||||
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
|
||||
AssertExistsAndLoadBean(t, &Comment{
|
||||
Type: CommentTypeLabel,
|
||||
PosterID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
LabelID: label1.ID,
|
||||
Content: "1",
|
||||
})
|
||||
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
|
||||
label1 = AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
|
||||
assert.EqualValues(t, 3, label1.NumIssues)
|
||||
assert.EqualValues(t, 1, label1.NumClosedIssues)
|
||||
label2 = AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
|
||||
assert.EqualValues(t, 1, label2.NumIssues)
|
||||
assert.EqualValues(t, 1, label2.NumClosedIssues)
|
||||
|
||||
// corner case: test empty slice
|
||||
assert.NoError(t, NewIssueLabels(issue, []*Label{}, doer))
|
||||
|
||||
CheckConsistencyFor(t, &Issue{}, &Label{})
|
||||
}
|
||||
|
||||
func TestDeleteIssueLabel(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
testSuccess := func(labelID, issueID, doerID int64) {
|
||||
label := AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label)
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: issueID}).(*Issue)
|
||||
doer := AssertExistsAndLoadBean(t, &User{ID: doerID}).(*User)
|
||||
|
||||
expectedNumIssues := label.NumIssues
|
||||
expectedNumClosedIssues := label.NumClosedIssues
|
||||
if BeanExists(t, &IssueLabel{IssueID: issueID, LabelID: labelID}) {
|
||||
expectedNumIssues--
|
||||
if issue.IsClosed {
|
||||
expectedNumClosedIssues--
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, DeleteIssueLabel(issue, label, doer))
|
||||
AssertNotExistsBean(t, &IssueLabel{IssueID: issueID, LabelID: labelID})
|
||||
AssertExistsAndLoadBean(t, &Comment{
|
||||
Type: CommentTypeLabel,
|
||||
PosterID: doerID,
|
||||
IssueID: issueID,
|
||||
LabelID: labelID,
|
||||
}, `content=""`)
|
||||
label = AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label)
|
||||
assert.EqualValues(t, expectedNumIssues, label.NumIssues)
|
||||
assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
|
||||
}
|
||||
testSuccess(1, 1, 2)
|
||||
testSuccess(2, 5, 2)
|
||||
testSuccess(1, 1, 2) // delete non-existent IssueLabel
|
||||
|
||||
CheckConsistencyFor(t, &Issue{}, &Label{})
|
||||
}
|
||||
320
models/issue_list.go
Normal file
320
models/issue_list.go
Normal file
@@ -0,0 +1,320 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// IssueList defines a list of issues
|
||||
type IssueList []*Issue
|
||||
|
||||
func (issues IssueList) getRepoIDs() []int64 {
|
||||
repoIDs := make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if _, ok := repoIDs[issue.RepoID]; !ok {
|
||||
repoIDs[issue.RepoID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return keysInt64(repoIDs)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadRepositories(e Engine) ([]*Repository, error) {
|
||||
if len(issues) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
repoIDs := issues.getRepoIDs()
|
||||
repoMaps := make(map[int64]*Repository, len(repoIDs))
|
||||
err := e.
|
||||
In("id", repoIDs).
|
||||
Find(&repoMaps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find repository: %v", err)
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Repo = repoMaps[issue.RepoID]
|
||||
}
|
||||
return valuesRepository(repoMaps), nil
|
||||
}
|
||||
|
||||
// LoadRepositories loads issues' all repositories
|
||||
func (issues IssueList) LoadRepositories() ([]*Repository, error) {
|
||||
return issues.loadRepositories(x)
|
||||
}
|
||||
|
||||
func (issues IssueList) getPosterIDs() []int64 {
|
||||
posterIDs := make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if _, ok := posterIDs[issue.PosterID]; !ok {
|
||||
posterIDs[issue.PosterID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return keysInt64(posterIDs)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPosters(e Engine) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
posterIDs := issues.getPosterIDs()
|
||||
posterMaps := make(map[int64]*User, len(posterIDs))
|
||||
err := e.
|
||||
In("id", posterIDs).
|
||||
Find(&posterMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Poster = posterMaps[issue.PosterID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getIssueIDs() []int64 {
|
||||
var ids = make([]int64, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
ids = append(ids, issue.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadLabels(e Engine) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type LabelIssue struct {
|
||||
Label *Label `xorm:"extends"`
|
||||
IssueLabel *IssueLabel `xorm:"extends"`
|
||||
}
|
||||
|
||||
var issueLabels = make(map[int64][]*Label, len(issues)*3)
|
||||
rows, err := e.Table("label").
|
||||
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
|
||||
In("issue_label.issue_id", issues.getIssueIDs()).
|
||||
Asc("label.name").
|
||||
Rows(new(LabelIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var labelIssue LabelIssue
|
||||
err = rows.Scan(&labelIssue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Labels = issueLabels[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getMilestoneIDs() []int64 {
|
||||
var ids = make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if _, ok := ids[issue.MilestoneID]; !ok {
|
||||
ids[issue.MilestoneID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return keysInt64(ids)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadMilestones(e Engine) error {
|
||||
milestoneIDs := issues.getMilestoneIDs()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
err := e.
|
||||
In("id", milestoneIDs).
|
||||
Find(&milestoneMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getAssigneeIDs() []int64 {
|
||||
var ids = make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if _, ok := ids[issue.AssigneeID]; !ok {
|
||||
ids[issue.AssigneeID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return keysInt64(ids)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAssignees(e Engine) error {
|
||||
assigneeIDs := issues.getAssigneeIDs()
|
||||
if len(assigneeIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
assigneeMaps := make(map[int64]*User, len(assigneeIDs))
|
||||
err := e.
|
||||
In("id", assigneeIDs).
|
||||
Find(&assigneeMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Assignee = assigneeMaps[issue.AssigneeID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getPullIssueIDs() []int64 {
|
||||
var ids = make([]int64, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.IsPull && issue.PullRequest == nil {
|
||||
ids = append(ids, issue.ID)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPullRequests(e Engine) error {
|
||||
issuesIDs := issues.getPullIssueIDs()
|
||||
if len(issuesIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
|
||||
rows, err := e.
|
||||
In("issue_id", issuesIDs).
|
||||
Rows(new(PullRequest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var pr PullRequest
|
||||
err = rows.Scan(&pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pullRequestMaps[pr.IssueID] = &pr
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.PullRequest = pullRequestMaps[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAttachments(e Engine) (err error) {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var attachments = make(map[int64][]*Attachment, len(issues))
|
||||
rows, err := e.Table("attachment").
|
||||
Join("INNER", "issue", "issue.id = attachment.issue_id").
|
||||
In("issue.id", issues.getIssueIDs()).
|
||||
Rows(new(Attachment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var attachment Attachment
|
||||
err = rows.Scan(&attachment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Attachments = attachments[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadComments(e Engine) (err error) {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var comments = make(map[int64][]*Comment, len(issues))
|
||||
rows, err := e.Table("comment").
|
||||
Join("INNER", "issue", "issue.id = comment.issue_id").
|
||||
In("issue.id", issues.getIssueIDs()).
|
||||
Rows(new(Comment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var comment Comment
|
||||
err = rows.Scan(&comment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Comments = comments[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAttributes(e Engine) (err error) {
|
||||
if _, err = issues.loadRepositories(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issues.loadPosters(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issues.loadLabels(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issues.loadMilestones(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issues.loadAssignees(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issues.loadPullRequests(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issues.loadAttachments(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issues.loadComments(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes loads atrributes of the issues
|
||||
func (issues IssueList) LoadAttributes() error {
|
||||
return issues.loadAttributes(x)
|
||||
}
|
||||
65
models/issue_list_test.go
Normal file
65
models/issue_list_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIssueList_LoadRepositories(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
issueList := IssueList{
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
|
||||
}
|
||||
|
||||
repos, err := issueList.LoadRepositories()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repos, 2)
|
||||
for _, issue := range issueList {
|
||||
assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
issueList := IssueList{
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
|
||||
AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
|
||||
}
|
||||
|
||||
assert.NoError(t, issueList.LoadAttributes())
|
||||
for _, issue := range issueList {
|
||||
assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
|
||||
for _, label := range issue.Labels {
|
||||
assert.EqualValues(t, issue.RepoID, label.RepoID)
|
||||
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label.ID})
|
||||
}
|
||||
if issue.PosterID > 0 {
|
||||
assert.EqualValues(t, issue.PosterID, issue.Poster.ID)
|
||||
}
|
||||
if issue.AssigneeID > 0 {
|
||||
assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID)
|
||||
}
|
||||
if issue.MilestoneID > 0 {
|
||||
assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID)
|
||||
}
|
||||
if issue.IsPull {
|
||||
assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID)
|
||||
}
|
||||
for _, attachment := range issue.Attachments {
|
||||
assert.EqualValues(t, issue.ID, attachment.IssueID)
|
||||
}
|
||||
for _, comment := range issue.Comments {
|
||||
assert.EqualValues(t, issue.ID, comment.IssueID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,27 @@ func (issue *Issue) mailSubject() string {
|
||||
}
|
||||
|
||||
// mailIssueCommentToParticipants can be used for both new issue creation and comment.
|
||||
// This function sends two list of emails:
|
||||
// 1. Repository watchers and users who are participated in comments.
|
||||
// 2. Users who are not in 1. but get mentioned in current issue/comment.
|
||||
func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) error {
|
||||
if !setting.Service.EnableNotifyMail {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mail wahtcers.
|
||||
watchers, err := GetWatchers(issue.RepoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetWatchers [%d]: %v", issue.RepoID, err)
|
||||
return fmt.Errorf("GetWatchers [repo_id: %d]: %v", issue.RepoID, err)
|
||||
}
|
||||
participants, err := GetParticipantsByIssueID(issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
|
||||
}
|
||||
|
||||
// In case the issue poster is not watching the repository,
|
||||
// even if we have duplicated in watchers, can be safely filtered out.
|
||||
if issue.PosterID != doer.ID {
|
||||
participants = append(participants, issue.Poster)
|
||||
}
|
||||
|
||||
tos := make([]string, 0, len(watchers)) // List of email addresses.
|
||||
@@ -48,6 +60,16 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string)
|
||||
tos = append(tos, to.Email)
|
||||
names = append(names, to.Name)
|
||||
}
|
||||
for i := range participants {
|
||||
if participants[i].ID == doer.ID {
|
||||
continue
|
||||
} else if com.IsSliceContainsStr(names, participants[i].Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
tos = append(tos, participants[i].Email)
|
||||
names = append(names, participants[i].Name)
|
||||
}
|
||||
SendIssueCommentMail(issue, doer, tos)
|
||||
|
||||
// Mail mentioned people and exclude watchers.
|
||||
|
||||
352
models/issue_milestone.go
Normal file
352
models/issue_milestone.go
Normal file
@@ -0,0 +1,352 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/sdk/gitea"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
// Milestone represents a milestone of repository.
|
||||
type Milestone struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
Content string `xorm:"TEXT"`
|
||||
RenderedContent string `xorm:"-"`
|
||||
IsClosed bool
|
||||
NumIssues int
|
||||
NumClosedIssues int
|
||||
NumOpenIssues int `xorm:"-"`
|
||||
Completeness int // Percentage(1-100).
|
||||
IsOverDue bool `xorm:"-"`
|
||||
|
||||
DeadlineString string `xorm:"-"`
|
||||
Deadline time.Time `xorm:"-"`
|
||||
DeadlineUnix int64
|
||||
ClosedDate time.Time `xorm:"-"`
|
||||
ClosedDateUnix int64
|
||||
}
|
||||
|
||||
// BeforeInsert is invoked from XORM before inserting an object of this type.
|
||||
func (m *Milestone) BeforeInsert() {
|
||||
m.DeadlineUnix = m.Deadline.Unix()
|
||||
}
|
||||
|
||||
// BeforeUpdate is invoked from XORM before updating this object.
|
||||
func (m *Milestone) BeforeUpdate() {
|
||||
if m.NumIssues > 0 {
|
||||
m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
|
||||
} else {
|
||||
m.Completeness = 0
|
||||
}
|
||||
|
||||
m.DeadlineUnix = m.Deadline.Unix()
|
||||
m.ClosedDateUnix = m.ClosedDate.Unix()
|
||||
}
|
||||
|
||||
// AfterSet is invoked from XORM after setting the value of a field of
|
||||
// this object.
|
||||
func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
|
||||
switch colName {
|
||||
case "num_closed_issues":
|
||||
m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
|
||||
|
||||
case "deadline_unix":
|
||||
m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
|
||||
if m.Deadline.Year() == 9999 {
|
||||
return
|
||||
}
|
||||
|
||||
m.DeadlineString = m.Deadline.Format("2006-01-02")
|
||||
if time.Now().Local().After(m.Deadline) {
|
||||
m.IsOverDue = true
|
||||
}
|
||||
|
||||
case "closed_date_unix":
|
||||
m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
|
||||
}
|
||||
}
|
||||
|
||||
// State returns string representation of milestone status.
|
||||
func (m *Milestone) State() api.StateType {
|
||||
if m.IsClosed {
|
||||
return api.StateClosed
|
||||
}
|
||||
return api.StateOpen
|
||||
}
|
||||
|
||||
// APIFormat returns this Milestone in API format.
|
||||
func (m *Milestone) APIFormat() *api.Milestone {
|
||||
apiMilestone := &api.Milestone{
|
||||
ID: m.ID,
|
||||
State: m.State(),
|
||||
Title: m.Name,
|
||||
Description: m.Content,
|
||||
OpenIssues: m.NumOpenIssues,
|
||||
ClosedIssues: m.NumClosedIssues,
|
||||
}
|
||||
if m.IsClosed {
|
||||
apiMilestone.Closed = &m.ClosedDate
|
||||
}
|
||||
if m.Deadline.Year() < 9999 {
|
||||
apiMilestone.Deadline = &m.Deadline
|
||||
}
|
||||
return apiMilestone
|
||||
}
|
||||
|
||||
// NewMilestone creates new milestone of repository.
|
||||
func NewMilestone(m *Milestone) (err error) {
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Insert(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
|
||||
return err
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
|
||||
m := &Milestone{
|
||||
ID: id,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := e.Get(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrMilestoneNotExist{id, repoID}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// GetMilestoneByRepoID returns the milestone in a repository.
|
||||
func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
|
||||
return getMilestoneByRepoID(x, repoID, id)
|
||||
}
|
||||
|
||||
// GetMilestonesByRepoID returns all milestones of a repository.
|
||||
func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
|
||||
miles := make([]*Milestone, 0, 10)
|
||||
return miles, x.Where("repo_id = ?", repoID).Find(&miles)
|
||||
}
|
||||
|
||||
// GetMilestones returns a list of milestones of given repository and status.
|
||||
func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*Milestone, error) {
|
||||
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
|
||||
sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
|
||||
if page > 0 {
|
||||
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
case "furthestduedate":
|
||||
sess.Desc("deadline_unix")
|
||||
case "leastcomplete":
|
||||
sess.Asc("completeness")
|
||||
case "mostcomplete":
|
||||
sess.Desc("completeness")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("deadline_unix")
|
||||
}
|
||||
|
||||
return miles, sess.Find(&miles)
|
||||
}
|
||||
|
||||
func updateMilestone(e Engine, m *Milestone) error {
|
||||
_, err := e.Id(m.ID).AllCols().Update(m)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateMilestone updates information of given milestone.
|
||||
func UpdateMilestone(m *Milestone) error {
|
||||
return updateMilestone(x, m)
|
||||
}
|
||||
|
||||
func countRepoMilestones(e Engine, repoID int64) int64 {
|
||||
count, _ := e.
|
||||
Where("repo_id=?", repoID).
|
||||
Count(new(Milestone))
|
||||
return count
|
||||
}
|
||||
|
||||
func countRepoClosedMilestones(e Engine, repoID int64) int64 {
|
||||
closed, _ := e.
|
||||
Where("repo_id=? AND is_closed=?", repoID, true).
|
||||
Count(new(Milestone))
|
||||
return closed
|
||||
}
|
||||
|
||||
// CountRepoClosedMilestones returns number of closed milestones in given repository.
|
||||
func CountRepoClosedMilestones(repoID int64) int64 {
|
||||
return countRepoClosedMilestones(x, repoID)
|
||||
}
|
||||
|
||||
// MilestoneStats returns number of open and closed milestones of given repository.
|
||||
func MilestoneStats(repoID int64) (open int64, closed int64) {
|
||||
open, _ = x.
|
||||
Where("repo_id=? AND is_closed=?", repoID, false).
|
||||
Count(new(Milestone))
|
||||
return open, CountRepoClosedMilestones(repoID)
|
||||
}
|
||||
|
||||
// ChangeMilestoneStatus changes the milestone open/closed status.
|
||||
func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
|
||||
repo, err := GetRepositoryByID(m.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.IsClosed = isClosed
|
||||
if err = updateMilestone(sess, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
|
||||
repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
|
||||
if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
|
||||
return err
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
|
||||
if issue.MilestoneID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if issue.IsClosed {
|
||||
m.NumOpenIssues--
|
||||
m.NumClosedIssues++
|
||||
} else {
|
||||
m.NumOpenIssues++
|
||||
m.NumClosedIssues--
|
||||
}
|
||||
|
||||
return updateMilestone(e, m)
|
||||
}
|
||||
|
||||
func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error {
|
||||
if oldMilestoneID > 0 {
|
||||
m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.NumIssues--
|
||||
if issue.IsClosed {
|
||||
m.NumClosedIssues--
|
||||
}
|
||||
|
||||
if err = updateMilestone(e, m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if issue.MilestoneID > 0 {
|
||||
m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.NumIssues++
|
||||
if issue.IsClosed {
|
||||
m.NumClosedIssues++
|
||||
}
|
||||
|
||||
if err = updateMilestone(e, m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := issue.loadRepo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldMilestoneID > 0 || issue.MilestoneID > 0 {
|
||||
if _, err := createMilestoneComment(e, doer, issue.Repo, issue, oldMilestoneID, issue.MilestoneID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return updateIssue(e, issue)
|
||||
}
|
||||
|
||||
// ChangeMilestoneAssign changes assignment of milestone for issue.
|
||||
func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil {
|
||||
return err
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// DeleteMilestoneByRepoID deletes a milestone from a repository.
|
||||
func DeleteMilestoneByRepoID(repoID, id int64) error {
|
||||
m, err := GetMilestoneByRepoID(repoID, id)
|
||||
if err != nil {
|
||||
if IsErrMilestoneNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
repo, err := GetRepositoryByID(m.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
|
||||
repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
|
||||
if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
240
models/issue_milestone_test.go
Normal file
240
models/issue_milestone_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "code.gitea.io/sdk/gitea"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMilestone_State(t *testing.T) {
|
||||
assert.Equal(t, api.StateOpen, (&Milestone{IsClosed: false}).State())
|
||||
assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State())
|
||||
}
|
||||
|
||||
func TestMilestone_APIFormat(t *testing.T) {
|
||||
milestone := &Milestone{
|
||||
ID: 3,
|
||||
RepoID: 4,
|
||||
Name: "milestoneName",
|
||||
Content: "milestoneContent",
|
||||
IsClosed: false,
|
||||
NumOpenIssues: 5,
|
||||
NumClosedIssues: 6,
|
||||
Deadline: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
assert.Equal(t, api.Milestone{
|
||||
ID: milestone.ID,
|
||||
State: api.StateOpen,
|
||||
Title: milestone.Name,
|
||||
Description: milestone.Content,
|
||||
OpenIssues: milestone.NumOpenIssues,
|
||||
ClosedIssues: milestone.NumClosedIssues,
|
||||
Deadline: &milestone.Deadline,
|
||||
}, *milestone.APIFormat())
|
||||
}
|
||||
|
||||
func TestNewMilestone(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
milestone := &Milestone{
|
||||
RepoID: 1,
|
||||
Name: "milestoneName",
|
||||
Content: "milestoneContent",
|
||||
}
|
||||
|
||||
assert.NoError(t, NewMilestone(milestone))
|
||||
AssertExistsAndLoadBean(t, milestone)
|
||||
CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{})
|
||||
}
|
||||
|
||||
func TestGetMilestoneByRepoID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
milestone, err := GetMilestoneByRepoID(1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, milestone.ID)
|
||||
assert.EqualValues(t, 1, milestone.RepoID)
|
||||
|
||||
_, err = GetMilestoneByRepoID(NonexistentID, NonexistentID)
|
||||
assert.True(t, IsErrMilestoneNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetMilestonesByRepoID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
milestones, err := GetMilestonesByRepoID(repo.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, milestones, repo.NumMilestones)
|
||||
for _, milestone := range milestones {
|
||||
assert.EqualValues(t, repoID, milestone.RepoID)
|
||||
}
|
||||
}
|
||||
test(1)
|
||||
test(2)
|
||||
test(3)
|
||||
|
||||
milestones, err := GetMilestonesByRepoID(NonexistentID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, milestones, 0)
|
||||
}
|
||||
|
||||
func TestGetMilestones(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||
test := func(sortType string, sortCond func(*Milestone) int) {
|
||||
for _, page := range []int{0, 1} {
|
||||
milestones, err := GetMilestones(repo.ID, page, false, sortType)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, milestones, repo.NumMilestones-repo.NumClosedMilestones)
|
||||
values := make([]int, len(milestones))
|
||||
for i, milestone := range milestones {
|
||||
values[i] = sortCond(milestone)
|
||||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
|
||||
milestones, err = GetMilestones(repo.ID, page, true, sortType)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, milestones, repo.NumClosedMilestones)
|
||||
values = make([]int, len(milestones))
|
||||
for i, milestone := range milestones {
|
||||
values[i] = sortCond(milestone)
|
||||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
}
|
||||
}
|
||||
test("furthestduedate", func(milestone *Milestone) int {
|
||||
return -int(milestone.DeadlineUnix)
|
||||
})
|
||||
test("leastcomplete", func(milestone *Milestone) int {
|
||||
return milestone.Completeness
|
||||
})
|
||||
test("mostcomplete", func(milestone *Milestone) int {
|
||||
return -milestone.Completeness
|
||||
})
|
||||
test("leastissues", func(milestone *Milestone) int {
|
||||
return milestone.NumIssues
|
||||
})
|
||||
test("mostissues", func(milestone *Milestone) int {
|
||||
return -milestone.NumIssues
|
||||
})
|
||||
test("soonestduedate", func(milestone *Milestone) int {
|
||||
return int(milestone.DeadlineUnix)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateMilestone(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
|
||||
milestone.Name = "newMilestoneName"
|
||||
milestone.Content = "newMilestoneContent"
|
||||
assert.NoError(t, UpdateMilestone(milestone))
|
||||
AssertExistsAndLoadBean(t, milestone)
|
||||
CheckConsistencyFor(t, &Milestone{})
|
||||
}
|
||||
|
||||
func TestCountRepoMilestones(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
assert.EqualValues(t, repo.NumMilestones, countRepoMilestones(x, repoID))
|
||||
}
|
||||
test(1)
|
||||
test(2)
|
||||
test(3)
|
||||
assert.EqualValues(t, 0, countRepoMilestones(x, NonexistentID))
|
||||
}
|
||||
|
||||
func TestCountRepoClosedMilestones(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, CountRepoClosedMilestones(repoID))
|
||||
}
|
||||
test(1)
|
||||
test(2)
|
||||
test(3)
|
||||
assert.EqualValues(t, 0, countRepoMilestones(x, NonexistentID))
|
||||
}
|
||||
|
||||
func TestMilestoneStats(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||
open, closed := MilestoneStats(repoID)
|
||||
assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, open)
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, closed)
|
||||
}
|
||||
test(1)
|
||||
test(2)
|
||||
test(3)
|
||||
|
||||
open, closed := MilestoneStats(NonexistentID)
|
||||
assert.EqualValues(t, 0, open)
|
||||
assert.EqualValues(t, 0, closed)
|
||||
}
|
||||
|
||||
func TestChangeMilestoneStatus(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
|
||||
|
||||
assert.NoError(t, ChangeMilestoneStatus(milestone, true))
|
||||
AssertExistsAndLoadBean(t, &Milestone{ID: 1}, "is_closed=1")
|
||||
CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{})
|
||||
|
||||
assert.NoError(t, ChangeMilestoneStatus(milestone, false))
|
||||
AssertExistsAndLoadBean(t, &Milestone{ID: 1}, "is_closed=0")
|
||||
CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{})
|
||||
}
|
||||
|
||||
func TestChangeMilestoneIssueStats(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1},
|
||||
"is_closed=0").(*Issue)
|
||||
|
||||
issue.IsClosed = true
|
||||
_, err := x.Cols("is_closed").Update(issue)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
|
||||
CheckConsistencyFor(t, &Milestone{})
|
||||
|
||||
issue.IsClosed = false
|
||||
_, err = x.Cols("is_closed").Update(issue)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
|
||||
CheckConsistencyFor(t, &Milestone{})
|
||||
}
|
||||
|
||||
func TestChangeMilestoneAssign(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue)
|
||||
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||
|
||||
oldMilestoneID := issue.MilestoneID
|
||||
issue.MilestoneID = 2
|
||||
assert.NoError(t, ChangeMilestoneAssign(issue, doer, oldMilestoneID))
|
||||
AssertExistsAndLoadBean(t, &Comment{
|
||||
IssueID: issue.ID,
|
||||
Type: CommentTypeMilestone,
|
||||
MilestoneID: issue.MilestoneID,
|
||||
OldMilestoneID: oldMilestoneID,
|
||||
})
|
||||
CheckConsistencyFor(t, &Milestone{}, &Issue{})
|
||||
}
|
||||
|
||||
func TestDeleteMilestoneByRepoID(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
assert.NoError(t, DeleteMilestoneByRepoID(1, 1))
|
||||
AssertNotExistsBean(t, &Milestone{ID: 1})
|
||||
CheckConsistencyFor(t, &Repository{ID: 1})
|
||||
|
||||
assert.NoError(t, DeleteMilestoneByRepoID(NonexistentID, NonexistentID))
|
||||
}
|
||||
86
models/issue_test.go
Normal file
86
models/issue_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIssue_ReplaceLabels(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(issueID int64, labelIDs []int64) {
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: issueID}).(*Issue)
|
||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: issue.RepoID}).(*Repository)
|
||||
doer := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
|
||||
|
||||
labels := make([]*Label, len(labelIDs))
|
||||
for i, labelID := range labelIDs {
|
||||
labels[i] = AssertExistsAndLoadBean(t, &Label{ID: labelID, RepoID: repo.ID}).(*Label)
|
||||
}
|
||||
assert.NoError(t, issue.ReplaceLabels(labels, doer))
|
||||
AssertCount(t, &IssueLabel{IssueID: issueID}, len(labelIDs))
|
||||
for _, labelID := range labelIDs {
|
||||
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issueID, LabelID: labelID})
|
||||
}
|
||||
}
|
||||
|
||||
testSuccess(1, []int64{2})
|
||||
testSuccess(1, []int64{1, 2})
|
||||
testSuccess(1, []int64{})
|
||||
}
|
||||
|
||||
func TestIssueAPIURL(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
|
||||
err := issue.LoadAttributes()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
|
||||
}
|
||||
|
||||
func TestGetIssuesByIDs(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
testSuccess := func(expectedIssueIDs []int64, nonExistentIssueIDs []int64) {
|
||||
issues, err := GetIssuesByIDs(append(expectedIssueIDs, nonExistentIssueIDs...))
|
||||
assert.NoError(t, err)
|
||||
actualIssueIDs := make([]int64, len(issues))
|
||||
for i, issue := range issues {
|
||||
actualIssueIDs[i] = issue.ID
|
||||
}
|
||||
assert.Equal(t, expectedIssueIDs, actualIssueIDs)
|
||||
|
||||
}
|
||||
testSuccess([]int64{1, 2, 3}, []int64{})
|
||||
testSuccess([]int64{1, 2, 3}, []int64{NonexistentID})
|
||||
}
|
||||
|
||||
func TestGetParticipantsByIssueID(t *testing.T) {
|
||||
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
checkPartecipants := func(issueID int64, userIDs []int) {
|
||||
partecipants, err := GetParticipantsByIssueID(issueID)
|
||||
if assert.NoError(t, err) {
|
||||
partecipantsIDs := make([]int, len(partecipants))
|
||||
for i, u := range partecipants {
|
||||
partecipantsIDs[i] = int(u.ID)
|
||||
}
|
||||
sort.Ints(partecipantsIDs)
|
||||
sort.Ints(userIDs)
|
||||
assert.Equal(t, userIDs, partecipantsIDs)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// User 1 is issue1 poster (see fixtures/issue.yml)
|
||||
// User 2 only labeled issue1 (see fixtures/comment.yml)
|
||||
// Users 3 and 5 made actual comments (see fixtures/comment.yml)
|
||||
checkPartecipants(1, []int{3, 5})
|
||||
|
||||
}
|
||||
113
models/issue_user.go
Normal file
113
models/issue_user.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// IssueUser represents an issue-user relation.
|
||||
type IssueUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX"` // User ID.
|
||||
IssueID int64
|
||||
IsRead bool
|
||||
IsAssigned bool
|
||||
IsMentioned bool
|
||||
}
|
||||
|
||||
func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
|
||||
assignees, err := repo.getAssignees(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAssignees: %v", err)
|
||||
}
|
||||
|
||||
// Poster can be anyone, append later if not one of assignees.
|
||||
isPosterAssignee := false
|
||||
|
||||
// Leave a seat for poster itself to append later, but if poster is one of assignee
|
||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
issueUsers := make([]*IssueUser, 0, len(assignees)+1)
|
||||
for _, assignee := range assignees {
|
||||
issueUsers = append(issueUsers, &IssueUser{
|
||||
IssueID: issue.ID,
|
||||
UID: assignee.ID,
|
||||
IsAssigned: assignee.ID == issue.AssigneeID,
|
||||
})
|
||||
isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID
|
||||
}
|
||||
if !isPosterAssignee {
|
||||
issueUsers = append(issueUsers, &IssueUser{
|
||||
IssueID: issue.ID,
|
||||
UID: issue.PosterID,
|
||||
})
|
||||
}
|
||||
|
||||
if _, err = e.Insert(issueUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateIssueUserByAssignee(e Engine, issue *Issue) (err error) {
|
||||
if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assignee ID equals to 0 means clear assignee.
|
||||
if issue.AssigneeID > 0 {
|
||||
if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return updateIssue(e, issue)
|
||||
}
|
||||
|
||||
// UpdateIssueUserByAssignee updates issue-user relation for assignee.
|
||||
func UpdateIssueUserByAssignee(issue *Issue) (err error) {
|
||||
sess := x.NewSession()
|
||||
defer sessionRelease(sess)
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = updateIssueUserByAssignee(sess, issue); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// UpdateIssueUserByRead updates issue-user relation for reading.
|
||||
func UpdateIssueUserByRead(uid, issueID int64) error {
|
||||
_, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
|
||||
func UpdateIssueUsersByMentions(e Engine, issueID int64, uids []int64) error {
|
||||
for _, uid := range uids {
|
||||
iu := &IssueUser{
|
||||
UID: uid,
|
||||
IssueID: issueID,
|
||||
}
|
||||
has, err := e.Get(iu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iu.IsMentioned = true
|
||||
if has {
|
||||
_, err = e.Id(iu.ID).AllCols().Update(iu)
|
||||
} else {
|
||||
_, err = e.Insert(iu)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user