mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-02 19:45:57 +01:00
Compare commits
1158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be79ac2eb2 | ||
|
|
05afec3236 | ||
|
|
57879eb72e | ||
|
|
2bc915f51b | ||
|
|
1ca55805b5 | ||
|
|
93cc1be166 | ||
|
|
f88ce3f671 | ||
|
|
20aabfc273 | ||
|
|
601f8c4249 | ||
|
|
d0ccfc52b8 | ||
|
|
c22aef8ee2 | ||
|
|
3807e61a48 | ||
|
|
55722f87af | ||
|
|
212f3725ed | ||
|
|
82beed1f44 | ||
|
|
0ede7e9921 | ||
|
|
d2317d0a97 | ||
|
|
972628eb65 | ||
|
|
51a56356cb | ||
|
|
2bb1f6168a | ||
|
|
b13820fc0e | ||
|
|
723de9e81e | ||
|
|
3e161353ed | ||
|
|
2a8706630a | ||
|
|
121b6ee641 | ||
|
|
34e299bf52 | ||
|
|
0822b7b5f3 | ||
|
|
618110327a | ||
|
|
f58f476060 | ||
|
|
f5a544603a | ||
|
|
89515cd087 | ||
|
|
37731c4163 | ||
|
|
1d4720d784 | ||
|
|
a10b053489 | ||
|
|
6122c8a1e1 | ||
|
|
fa9254c240 | ||
|
|
10616bca7d | ||
|
|
307f7e15e9 | ||
|
|
86cf97d76b | ||
|
|
01f6590c04 | ||
|
|
8f0c22bae9 | ||
|
|
652a68c5b1 | ||
|
|
1f56e1360d | ||
|
|
38475ffefe | ||
|
|
7a44a4d726 | ||
|
|
9dbc0c3fd6 | ||
|
|
56bb43ea6b | ||
|
|
b287c1f60d | ||
|
|
258d53b7a6 | ||
|
|
2e11d6dd78 | ||
|
|
a2a2e22485 | ||
|
|
c182cde14b | ||
|
|
104c3bc89d | ||
|
|
2668977918 | ||
|
|
28424c96c4 | ||
|
|
9cfa8c594b | ||
|
|
5c70cd654c | ||
|
|
7aca24e51d | ||
|
|
cce0b67871 | ||
|
|
606cd83f44 | ||
|
|
32897c36f9 | ||
|
|
92e4e12655 | ||
|
|
c8e5b75165 | ||
|
|
09b9a52ad3 | ||
|
|
33378c6464 | ||
|
|
259bcfc14f | ||
|
|
c361d24ba4 | ||
|
|
d5e1b18b52 | ||
|
|
684a17a15b | ||
|
|
66b7b69d20 | ||
|
|
57254f6366 | ||
|
|
c64909ab1a | ||
|
|
34dd8541f4 | ||
|
|
50b4fb154d | ||
|
|
0b3781ec8a | ||
|
|
0e1d184715 | ||
|
|
d8c27046f6 | ||
|
|
fd09058a7d | ||
|
|
1c99b57709 | ||
|
|
9ee739d102 | ||
|
|
e2cde81b72 | ||
|
|
84a4b8fd92 | ||
|
|
d2c94909cb | ||
|
|
3683a5fb7d | ||
|
|
1223bf2fd8 | ||
|
|
a9bfe0dfab | ||
|
|
9af81c7093 | ||
|
|
1e8a5c3cde | ||
|
|
707ad866e1 | ||
|
|
c3a944b40e | ||
|
|
ab80cb8f60 | ||
|
|
4f45e047d2 | ||
|
|
bbe455ac49 | ||
|
|
b5f173fa46 | ||
|
|
4bd6ef143a | ||
|
|
fd4a696303 | ||
|
|
4af4c4e7c6 | ||
|
|
3b2e42fd61 | ||
|
|
b07d0b028f | ||
|
|
f3900ca8f9 | ||
|
|
62d43f120a | ||
|
|
c4f69fbd13 | ||
|
|
fece20ff40 | ||
|
|
bbef4b22ca | ||
|
|
481a2d213f | ||
|
|
8ed4075f1e | ||
|
|
9bf82733d1 | ||
|
|
30d66f95bc | ||
|
|
378c2c39a8 | ||
|
|
daf5fc434c | ||
|
|
e5bf90ed26 | ||
|
|
1bf3146220 | ||
|
|
ddd51850f0 | ||
|
|
e14a0c3770 | ||
|
|
b0b318ce30 | ||
|
|
6f666ca49f | ||
|
|
0cb2116bdf | ||
|
|
280113497b | ||
|
|
5f6e318329 | ||
|
|
f8921b6f10 | ||
|
|
31a08abff2 | ||
|
|
0fa1e11c5a | ||
|
|
e2c99a46be | ||
|
|
1edff41690 | ||
|
|
6d6f529d40 | ||
|
|
e2fd7d9d8e | ||
|
|
61146687b3 | ||
|
|
1d1f7fa581 | ||
|
|
67da88fab5 | ||
|
|
fb3ed70215 | ||
|
|
2fceeeee4e | ||
|
|
67102822e8 | ||
|
|
d00a0f1571 | ||
|
|
db5395ddbc | ||
|
|
7698f12112 | ||
|
|
1e8224536b | ||
|
|
a846c77c7e | ||
|
|
29812f4a82 | ||
|
|
a863951d97 | ||
|
|
146be677ba | ||
|
|
03b5f7feb8 | ||
|
|
6d54361a6d | ||
|
|
f440421ed1 | ||
|
|
e57464fc5e | ||
|
|
2a4b0f5ddb | ||
|
|
bb66e2201f | ||
|
|
4dc60e887f | ||
|
|
f6eb2e2dc8 | ||
|
|
9ecc10ab21 | ||
|
|
d7037a43c6 | ||
|
|
2471b8dfe0 | ||
|
|
0430cb49f9 | ||
|
|
7811926779 | ||
|
|
9bb66a4297 | ||
|
|
70772f0d74 | ||
|
|
728b00e4c3 | ||
|
|
97008ef984 | ||
|
|
6b86406e94 | ||
|
|
4252c364a4 | ||
|
|
4f4bc0321b | ||
|
|
6ecabe4588 | ||
|
|
93fa8484c5 | ||
|
|
ff2e55e82c | ||
|
|
259637ce3c | ||
|
|
743b9b759a | ||
|
|
73ba0b348b | ||
|
|
e93769cc81 | ||
|
|
68f9739eed | ||
|
|
c3d25b7a71 | ||
|
|
aaa582ff1a | ||
|
|
debc798aec | ||
|
|
6042f0e1e0 | ||
|
|
e10d02f45c | ||
|
|
aebf4ff728 | ||
|
|
1a2e89c9ed | ||
|
|
e10e2748b9 | ||
|
|
f422936e34 | ||
|
|
4e87f21405 | ||
|
|
dc2d79b16c | ||
|
|
88a3100563 | ||
|
|
8d3433a0e7 | ||
|
|
0fe30e5629 | ||
|
|
ea1e9037c4 | ||
|
|
24feeb17be | ||
|
|
6a7fc55572 | ||
|
|
cf047a8cee | ||
|
|
896420f8dc | ||
|
|
619f72d929 | ||
|
|
dc21e8388e | ||
|
|
8c35310cd6 | ||
|
|
642e8bbb7c | ||
|
|
3ee4143235 | ||
|
|
c136823170 | ||
|
|
92631fbfcf | ||
|
|
5a1b1a4485 | ||
|
|
3e82534c78 | ||
|
|
dd694d27b5 | ||
|
|
1900aefe32 | ||
|
|
2fe6b8c1e7 | ||
|
|
ecfaa0247a | ||
|
|
9a0cc9e043 | ||
|
|
b0360db105 | ||
|
|
0f9c95c15a | ||
|
|
8efd1da7e6 | ||
|
|
52ebba43d5 | ||
|
|
790eee7443 | ||
|
|
9f325290e8 | ||
|
|
93bf0a9a47 | ||
|
|
bdd0af21a9 | ||
|
|
aae5fe387b | ||
|
|
257c5aef51 | ||
|
|
3cae337487 | ||
|
|
779df30ec8 | ||
|
|
5609507991 | ||
|
|
1c24090c14 | ||
|
|
7da2c650d2 | ||
|
|
27fa9df2ee | ||
|
|
63c4e12259 | ||
|
|
1f66670819 | ||
|
|
a7b4f8de8d | ||
|
|
ad0d57fbf9 | ||
|
|
cfc594805b | ||
|
|
52461e673c | ||
|
|
a97edb7ef5 | ||
|
|
7a1c872861 | ||
|
|
0e5591017a | ||
|
|
a104157c9a | ||
|
|
ad244adbfa | ||
|
|
3721b328a6 | ||
|
|
dd688f48b7 | ||
|
|
296a0b2124 | ||
|
|
b9cc46e5ef | ||
|
|
375211fc30 | ||
|
|
b8b59f9dcd | ||
|
|
6760ff34ef | ||
|
|
c5de7811c4 | ||
|
|
82ef5457b0 | ||
|
|
d558476cd2 | ||
|
|
644701d995 | ||
|
|
1382d59206 | ||
|
|
b60e2c07c7 | ||
|
|
86f0307633 | ||
|
|
1db891a771 | ||
|
|
c9fa3291f5 | ||
|
|
e0f1658120 | ||
|
|
da105b7180 | ||
|
|
9c4f7cc530 | ||
|
|
d7eef8bd25 | ||
|
|
7b7c0e1eee | ||
|
|
2ae7798591 | ||
|
|
3f76453f34 | ||
|
|
8fbbe7f31e | ||
|
|
92a43b4f99 | ||
|
|
c128086778 | ||
|
|
cc4fb8bf79 | ||
|
|
c3ac0f3d9f | ||
|
|
dfa4816633 | ||
|
|
06978a4fc4 | ||
|
|
3a2ecf6896 | ||
|
|
b357d52ec5 | ||
|
|
f8b6b1ebf8 | ||
|
|
91bd9d1111 | ||
|
|
1ec825050d | ||
|
|
a6a08d13e9 | ||
|
|
9a47c4a990 | ||
|
|
5063294177 | ||
|
|
b14917e2c6 | ||
|
|
c1bbec2a1c | ||
|
|
6227a4643a | ||
|
|
00af52815d | ||
|
|
5d3365a944 | ||
|
|
84ac2974fb | ||
|
|
c9a1515d1f | ||
|
|
5317ac5e03 | ||
|
|
9df1467ddf | ||
|
|
df79bd4515 | ||
|
|
cbb14f2ba8 | ||
|
|
1fe649e70f | ||
|
|
0d918add28 | ||
|
|
3926c98338 | ||
|
|
3bff6a1949 | ||
|
|
ec0c964ceb | ||
|
|
b4fd90c6d3 | ||
|
|
7dfd63cfa2 | ||
|
|
a562e5ca14 | ||
|
|
2885eef4ab | ||
|
|
087297d14c | ||
|
|
6e0fb95ac3 | ||
|
|
61e28146fb | ||
|
|
40d3f0ef9e | ||
|
|
99db825114 | ||
|
|
7341b377fe | ||
|
|
7f78a98de0 | ||
|
|
a64207f0ec | ||
|
|
d86f40e3a2 | ||
|
|
b74417f393 | ||
|
|
f5883abf04 | ||
|
|
02a367fd99 | ||
|
|
4870533710 | ||
|
|
9175cf5c71 | ||
|
|
8170a1b01d | ||
|
|
d1c6c763e2 | ||
|
|
0c683f7243 | ||
|
|
63de780527 | ||
|
|
c5ccbf2d1f | ||
|
|
8777535431 | ||
|
|
70192ce420 | ||
|
|
a74bbd3eeb | ||
|
|
02d79cb16a | ||
|
|
78ca9b3f1a | ||
|
|
017631e337 | ||
|
|
f9078dff2c | ||
|
|
b66381d677 | ||
|
|
49bf88f7a7 | ||
|
|
f93ceaa91d | ||
|
|
0fe122dc63 | ||
|
|
4e2a3fdbd0 | ||
|
|
3d251fa8ad | ||
|
|
af0b52448a | ||
|
|
8d200c72d3 | ||
|
|
f78cdb637d | ||
|
|
845f2d6faa | ||
|
|
525edbab80 | ||
|
|
c422b1c9a5 | ||
|
|
1043b13228 | ||
|
|
5e6c33df6c | ||
|
|
9541771703 | ||
|
|
f99d37cfad | ||
|
|
0cfe31ccd9 | ||
|
|
8fc1a5473b | ||
|
|
049b12b908 | ||
|
|
45f992b2bc | ||
|
|
9e2c66c341 | ||
|
|
2d0f59b6f2 | ||
|
|
fbba29e810 | ||
|
|
07a108760c | ||
|
|
b641bfb56a | ||
|
|
c65d80bc72 | ||
|
|
e0d266bf16 | ||
|
|
b62f7c5aee | ||
|
|
c89f04b926 | ||
|
|
ff8b4b4a88 | ||
|
|
07d63ae63a | ||
|
|
c0f5cb1641 | ||
|
|
50d84835cb | ||
|
|
8cdf4ef618 | ||
|
|
eff3a7acb4 | ||
|
|
18cd967a9c | ||
|
|
328d6c1d17 | ||
|
|
716eddac7b | ||
|
|
9b15af3bb7 | ||
|
|
b732e0d55a | ||
|
|
d92a1cee1c | ||
|
|
10a40bfcaf | ||
|
|
af397ba150 | ||
|
|
c7a2ec8290 | ||
|
|
145c155ba5 | ||
|
|
6f9ef32d96 | ||
|
|
aa5b9dbbbd | ||
|
|
f11be44c02 | ||
|
|
4276c8f23e | ||
|
|
9e1352c8b1 | ||
|
|
d46589ad29 | ||
|
|
09b7e67c52 | ||
|
|
79e1abe624 | ||
|
|
3db3bf1b74 | ||
|
|
a335c31385 | ||
|
|
9bd1f0a492 | ||
|
|
7a2c82461e | ||
|
|
21f7888f55 | ||
|
|
97349a9bb2 | ||
|
|
ce3b6ed7c2 | ||
|
|
e3fd564efd | ||
|
|
5cf96134d5 | ||
|
|
607c477e7d | ||
|
|
5e0619b500 | ||
|
|
17920e1195 | ||
|
|
721454aa90 | ||
|
|
d870896cfb | ||
|
|
270eb7cf1d | ||
|
|
527fd94145 | ||
|
|
04e4572088 | ||
|
|
0961eb5976 | ||
|
|
0311359922 | ||
|
|
ec09adf03e | ||
|
|
b031103df8 | ||
|
|
7701521a2e | ||
|
|
0c683d4f75 | ||
|
|
200d095034 | ||
|
|
94576a876a | ||
|
|
0fa1922bb0 | ||
|
|
c557905858 | ||
|
|
31b21d74b1 | ||
|
|
153244c390 | ||
|
|
e97b5c3c89 | ||
|
|
374893a5ae | ||
|
|
17f581f654 | ||
|
|
590b431ec1 | ||
|
|
98266fe0e1 | ||
|
|
2e236e90ba | ||
|
|
c5aee0810c | ||
|
|
f13d757976 | ||
|
|
7a0a62af2d | ||
|
|
ceab1d2fd2 | ||
|
|
639e7e0b3f | ||
|
|
89601305f6 | ||
|
|
4600b5a3bf | ||
|
|
b620307983 | ||
|
|
891ca70ade | ||
|
|
9ed2a50d26 | ||
|
|
cbf615d699 | ||
|
|
97b1a0090d | ||
|
|
9078aa6d08 | ||
|
|
8677146a8d | ||
|
|
2c14dfb781 | ||
|
|
057c5f073c | ||
|
|
e902da6595 | ||
|
|
8b5414c8f7 | ||
|
|
c86ece4dc0 | ||
|
|
1f71619b6b | ||
|
|
5b34b9c795 | ||
|
|
99d15899f6 | ||
|
|
c114a8b507 | ||
|
|
0dd37c2481 | ||
|
|
b5d7c96bba | ||
|
|
a76792ced4 | ||
|
|
39091240ff | ||
|
|
0ccb753892 | ||
|
|
63dda84c8b | ||
|
|
7ba1f85d48 | ||
|
|
bb9a23fe0f | ||
|
|
8536824d7e | ||
|
|
78073babe4 | ||
|
|
521d15219c | ||
|
|
7469a3c349 | ||
|
|
153a32e340 | ||
|
|
f155d4f150 | ||
|
|
d683dd2c38 | ||
|
|
7ebba741a8 | ||
|
|
d10f683098 | ||
|
|
0270133ecf | ||
|
|
d7b479d97d | ||
|
|
4366c512fe | ||
|
|
229a773ed2 | ||
|
|
d882f20436 | ||
|
|
9d7235af20 | ||
|
|
c2eb53d154 | ||
|
|
7629e347df | ||
|
|
2764caae29 | ||
|
|
a87bd2a928 | ||
|
|
202c920064 | ||
|
|
a08316bba0 | ||
|
|
520e5ebb7a | ||
|
|
5d5a4cacb1 | ||
|
|
b885a1a0d4 | ||
|
|
1705bd3ae9 | ||
|
|
e87c69f989 | ||
|
|
1c529eea3d | ||
|
|
738b0cfe9a | ||
|
|
913561cb2a | ||
|
|
05a91565dc | ||
|
|
79827efe9b | ||
|
|
8722cd89fc | ||
|
|
52fcc4ad1e | ||
|
|
59a096bfd6 | ||
|
|
5a1f541e13 | ||
|
|
94bd1c6a93 | ||
|
|
5b1aef5e52 | ||
|
|
89bfcdc44e | ||
|
|
fba81138ea | ||
|
|
d50e07265e | ||
|
|
c92891538e | ||
|
|
ccc1e9bc8b | ||
|
|
f33b398428 | ||
|
|
226a8af262 | ||
|
|
ebcc5ab4b1 | ||
|
|
10e16e8379 | ||
|
|
df1f3d8a00 | ||
|
|
5e2dfffe25 | ||
|
|
897f2ea6dd | ||
|
|
3ff39ec578 | ||
|
|
3d852a535d | ||
|
|
6f6a61f31a | ||
|
|
10f54f5790 | ||
|
|
0e7280585a | ||
|
|
1da7173f27 | ||
|
|
1cb1e68a01 | ||
|
|
b59c8a5512 | ||
|
|
fe63ad0976 | ||
|
|
941cb7b851 | ||
|
|
d1cf0d9fd7 | ||
|
|
64c2bb4d6b | ||
|
|
24c9f5c17e | ||
|
|
d368e4e80d | ||
|
|
5c0ff84fc4 | ||
|
|
502a21b6b6 | ||
|
|
0e9bf59c0f | ||
|
|
108f9fccdd | ||
|
|
ac884bd7c3 | ||
|
|
a4cb5c991c | ||
|
|
68f1f55f37 | ||
|
|
1dc779d5e8 | ||
|
|
f781c7a08c | ||
|
|
a8511a9f39 | ||
|
|
47714eec45 | ||
|
|
c46e9b2f4d | ||
|
|
26d579f13f | ||
|
|
6556d26742 | ||
|
|
608dce2205 | ||
|
|
f86e50c723 | ||
|
|
b60fe33886 | ||
|
|
5210a143fd | ||
|
|
dc78dc9b0d | ||
|
|
6b11c1a180 | ||
|
|
b3669f6d66 | ||
|
|
bbff75e037 | ||
|
|
7e10618ceb | ||
|
|
7f4def6b83 | ||
|
|
5790d246c8 | ||
|
|
19dee09c86 | ||
|
|
dfe2889912 | ||
|
|
223ba791fe | ||
|
|
0d49bbe7ac | ||
|
|
8381e8122a | ||
|
|
f38924c7fe | ||
|
|
43152c9341 | ||
|
|
cf84e8b7cc | ||
|
|
2b42e73530 | ||
|
|
60030959f2 | ||
|
|
7174523ac5 | ||
|
|
f573fef9eb | ||
|
|
b4250d8254 | ||
|
|
ac4d4de3c1 | ||
|
|
05e6d008fa | ||
|
|
dd4abb2073 | ||
|
|
612aba1365 | ||
|
|
94dce09570 | ||
|
|
cc241c5a7b | ||
|
|
13cf9d01f0 | ||
|
|
47453fec3f | ||
|
|
641d506559 | ||
|
|
3dec2b8159 | ||
|
|
a0bd969140 | ||
|
|
b30d42a37b | ||
|
|
a03acc68e7 | ||
|
|
05296473d3 | ||
|
|
2118f8c764 | ||
|
|
e366af98b5 | ||
|
|
81e2ac44c3 | ||
|
|
07bb326c06 | ||
|
|
bcc2c8cc2d | ||
|
|
2e0e17f1aa | ||
|
|
c517b44e82 | ||
|
|
f311339786 | ||
|
|
34853d0322 | ||
|
|
9c60b69c88 | ||
|
|
4f10bccf84 | ||
|
|
c7eaebf597 | ||
|
|
60e1052d33 | ||
|
|
7e77c102b0 | ||
|
|
a452c582ab | ||
|
|
0d3adb074d | ||
|
|
8ec4b52dda | ||
|
|
9265c68383 | ||
|
|
4bd2d78ecb | ||
|
|
e7aa766d0a | ||
|
|
7d8300b3ce | ||
|
|
af8a1234ed | ||
|
|
bd0ecd0a9d | ||
|
|
35c8f02f90 | ||
|
|
f160952817 | ||
|
|
9e5a302ab1 | ||
|
|
a1dc19fa26 | ||
|
|
e79ded934f | ||
|
|
ef3e7d9286 | ||
|
|
68b25ddbb5 | ||
|
|
f96040eade | ||
|
|
599a808054 | ||
|
|
382c5c55ec | ||
|
|
afb2306904 | ||
|
|
2642da3be3 | ||
|
|
dcbf283c9d | ||
|
|
f38fa0132c | ||
|
|
569053f7e0 | ||
|
|
037a97ff3d | ||
|
|
6e169ab3c2 | ||
|
|
6ac27e89b3 | ||
|
|
2235dab550 | ||
|
|
7604c2172f | ||
|
|
1e750f4b9d | ||
|
|
d1f0d01ae8 | ||
|
|
167a0f28b2 | ||
|
|
06be5266fd | ||
|
|
60e7165983 | ||
|
|
6dbfc12896 | ||
|
|
6d4b3e54d0 | ||
|
|
2968b92677 | ||
|
|
0d0bf4ad3f | ||
|
|
53fa60b0f8 | ||
|
|
99517fa508 | ||
|
|
2e239d16d4 | ||
|
|
6de5babd5b | ||
|
|
f3ad1a019d | ||
|
|
90ab882e8e | ||
|
|
53269096a6 | ||
|
|
254509f243 | ||
|
|
a697f186af | ||
|
|
2316a80be9 | ||
|
|
bbcb04b263 | ||
|
|
7afe7fbb5f | ||
|
|
7c7da7379d | ||
|
|
37358e9c8c | ||
|
|
41941df87a | ||
|
|
bf2ed81eb1 | ||
|
|
2d85d41e9c | ||
|
|
e5e7b2484c | ||
|
|
6058552654 | ||
|
|
f40c7ff4fa | ||
|
|
da62c6181e | ||
|
|
4d066738eb | ||
|
|
cb12d03262 | ||
|
|
9a6a2d9b78 | ||
|
|
ff0af477cb | ||
|
|
05adf9345f | ||
|
|
ba70fdda48 | ||
|
|
3885fcb2ec | ||
|
|
99800a27f5 | ||
|
|
107622942b | ||
|
|
9794f14a65 | ||
|
|
af759a815f | ||
|
|
0e7078c479 | ||
|
|
83107c7974 | ||
|
|
ff9b2dbe93 | ||
|
|
ebf4e5f2e9 | ||
|
|
21c30583e5 | ||
|
|
d6c9ace306 | ||
|
|
faf1252597 | ||
|
|
7b2ee25ea2 | ||
|
|
5a3207ae42 | ||
|
|
3eab4955b9 | ||
|
|
d772fc3ba2 | ||
|
|
7de0a3fd70 | ||
|
|
eb8710a336 | ||
|
|
25c55ecbd0 | ||
|
|
280df2cedd | ||
|
|
5ba9c86bee | ||
|
|
faa6591d27 | ||
|
|
841d442f0d | ||
|
|
3351eabc4f | ||
|
|
006e1bc61e | ||
|
|
35d1b4ea37 | ||
|
|
b0c5069695 | ||
|
|
dae0d0ad4b | ||
|
|
79e560b7bf | ||
|
|
cf79ac1069 | ||
|
|
8aab7a16c4 | ||
|
|
c16b89b0be | ||
|
|
25bbc00ff3 | ||
|
|
e667b6c139 | ||
|
|
195364223f | ||
|
|
84ce2cac8d | ||
|
|
f3507cf465 | ||
|
|
f74f2c47d3 | ||
|
|
72b25591a5 | ||
|
|
fe23a5c6da | ||
|
|
49fbc5cb62 | ||
|
|
5a19a307a9 | ||
|
|
c3ec52b391 | ||
|
|
f2d68be0a3 | ||
|
|
c1f98ac481 | ||
|
|
8287c84dc7 | ||
|
|
13bff2963e | ||
|
|
035f3f9e02 | ||
|
|
65e6de5ba4 | ||
|
|
82ced9233a | ||
|
|
e94411ebeb | ||
|
|
b92b429ffa | ||
|
|
e457cfb212 | ||
|
|
f1476c52e6 | ||
|
|
332246aed6 | ||
|
|
1c5201dcf1 | ||
|
|
36880ace27 | ||
|
|
0d55d6ef6b | ||
|
|
688bf645b4 | ||
|
|
d5a14482a6 | ||
|
|
cc1e0030df | ||
|
|
fcadcb34a2 | ||
|
|
dd8f440be0 | ||
|
|
17bc422e7a | ||
|
|
380cdbcf75 | ||
|
|
f4f2bf34fc | ||
|
|
ed713d80a9 | ||
|
|
c39703c61c | ||
|
|
537773f975 | ||
|
|
f37eca7c61 | ||
|
|
40a52d5ad5 | ||
|
|
d95bd20cbe | ||
|
|
70ca98d6a2 | ||
|
|
cf7caf55da | ||
|
|
b74bff3b2e | ||
|
|
b2e4853976 | ||
|
|
aef3c5c121 | ||
|
|
4afbfcb016 | ||
|
|
09f8cff4c9 | ||
|
|
9ecd162040 | ||
|
|
8617f02b01 | ||
|
|
9d71d39917 | ||
|
|
5430564065 | ||
|
|
54bc8c16d8 | ||
|
|
9c14ddda18 | ||
|
|
0affdb6ad0 | ||
|
|
532978522a | ||
|
|
05a9a0b45c | ||
|
|
24f8ad11ad | ||
|
|
ce943a0e6c | ||
|
|
204c0cd0f8 | ||
|
|
c213008f1c | ||
|
|
e6ad069509 | ||
|
|
38c7e3cdf8 | ||
|
|
2be79f6590 | ||
|
|
2f7125b6c0 | ||
|
|
bb03a6fc9b | ||
|
|
7b774aee1a | ||
|
|
d53619c247 | ||
|
|
d34118bdfd | ||
|
|
c57bc487a3 | ||
|
|
296fc9a3df | ||
|
|
fd8b5780f3 | ||
|
|
602b6c635a | ||
|
|
a79180699e | ||
|
|
e9901a8abf | ||
|
|
4e63d64c13 | ||
|
|
4261b7adbe | ||
|
|
f30c9f6171 | ||
|
|
c00b704843 | ||
|
|
e89b2020a3 | ||
|
|
18ca3cbd80 | ||
|
|
062d6cd066 | ||
|
|
b4dd067d61 | ||
|
|
fd22e2911a | ||
|
|
73d9e69e43 | ||
|
|
7e4c29f4cf | ||
|
|
32672262ef | ||
|
|
3c865ea20b | ||
|
|
d8698d02b7 | ||
|
|
d5b47e5adb | ||
|
|
accb1cf2ab | ||
|
|
aa8da1b046 | ||
|
|
c52ed32949 | ||
|
|
ec6f4ff734 | ||
|
|
06b0dbf2e5 | ||
|
|
98d24248c2 | ||
|
|
cec1dc98a9 | ||
|
|
36115734bb | ||
|
|
c1eccd391d | ||
|
|
7fe86fcdb2 | ||
|
|
7f81ec52c1 | ||
|
|
7c269de39b | ||
|
|
aa9e34e992 | ||
|
|
4d0ab514fb | ||
|
|
9d526b32e0 | ||
|
|
90a83c5c64 | ||
|
|
e6e5cc67d5 | ||
|
|
4a6eb95474 | ||
|
|
7bce8cf3b6 | ||
|
|
4d1605ded2 | ||
|
|
2bec2cfa93 | ||
|
|
ff07872a3d | ||
|
|
35733cd82e | ||
|
|
c88b051121 | ||
|
|
38df990033 | ||
|
|
c7776b5b37 | ||
|
|
f89afc175f | ||
|
|
1f252efdfb | ||
|
|
420ca85393 | ||
|
|
d60695992b | ||
|
|
3c0681d55d | ||
|
|
3fc0fa5a02 | ||
|
|
d84d40afea | ||
|
|
ddbbd38517 | ||
|
|
d588531ab8 | ||
|
|
bdc06feb88 | ||
|
|
940e2f4759 | ||
|
|
3fc792fcf8 | ||
|
|
f5520e7991 | ||
|
|
897c5ecac7 | ||
|
|
4479ef31e2 | ||
|
|
ec827ab371 | ||
|
|
6fe65c76b1 | ||
|
|
e79d463cf7 | ||
|
|
1508e5db49 | ||
|
|
8de391825a | ||
|
|
da1172a882 | ||
|
|
288a434598 | ||
|
|
1d6ae1e589 | ||
|
|
dd29456384 | ||
|
|
95a658defa | ||
|
|
cd298eb5c1 | ||
|
|
f7de3bab74 | ||
|
|
13578dcee8 | ||
|
|
6d76e93ede | ||
|
|
6b57cca64d | ||
|
|
e0bd5a24f4 | ||
|
|
2b2bf88a37 | ||
|
|
a0fa53e8cb | ||
|
|
c841d4a77a | ||
|
|
bf4b2dc72c | ||
|
|
078ed868fb | ||
|
|
bfc1d1d6b0 | ||
|
|
42ecae944e | ||
|
|
b9aa6a234b | ||
|
|
5f2d62030f | ||
|
|
fd0169d012 | ||
|
|
7e26b4695d | ||
|
|
cdfdff5c32 | ||
|
|
df5600f03f | ||
|
|
231fd268df | ||
|
|
582df3239f | ||
|
|
3ea102e238 | ||
|
|
52ab3c625e | ||
|
|
dee13542cd | ||
|
|
e90ba9e65b | ||
|
|
ca86076a02 | ||
|
|
6c75a29cb0 | ||
|
|
e10777576f | ||
|
|
08eaf2104b | ||
|
|
14de86afa0 | ||
|
|
69c5f9e19a | ||
|
|
03e903eef9 | ||
|
|
f3a1815bc5 | ||
|
|
ef03f77dc9 | ||
|
|
1a509a9a13 | ||
|
|
1e566f4a20 | ||
|
|
709c8f32b5 | ||
|
|
f2787a547f | ||
|
|
629b714dab | ||
|
|
1b0269c567 | ||
|
|
6158dc9607 | ||
|
|
5462f0a7a1 | ||
|
|
6d453ea80b | ||
|
|
5952648fae | ||
|
|
6b49bd557f | ||
|
|
c071284a56 | ||
|
|
5930cf48d5 | ||
|
|
9dd070887a | ||
|
|
cf687a0f2c | ||
|
|
f5c0cfdcdd | ||
|
|
03e2974709 | ||
|
|
d2373a00ea | ||
|
|
e769460397 | ||
|
|
a0a284ad26 | ||
|
|
1ebf4276e7 | ||
|
|
908931b9ed | ||
|
|
50655d1ac2 | ||
|
|
92e19ee19f | ||
|
|
52f3a90d18 | ||
|
|
11371c9e4f | ||
|
|
1b71b81953 | ||
|
|
c9d9d22215 | ||
|
|
5300641822 | ||
|
|
b31b7e1e86 | ||
|
|
cfb2f5beb9 | ||
|
|
ee9f24b2a6 | ||
|
|
8c86e23a4c | ||
|
|
fe98d35d4e | ||
|
|
8e10693402 | ||
|
|
f31848721c | ||
|
|
6101e141d8 | ||
|
|
71d84e7475 | ||
|
|
735ad4c972 | ||
|
|
50cb59f569 | ||
|
|
b58c19b88b | ||
|
|
6fe9ebbd2d | ||
|
|
4ea23a96ae | ||
|
|
ebf5d00fd2 | ||
|
|
b015645ed0 | ||
|
|
ce3b10ef03 | ||
|
|
d7af5551eb | ||
|
|
1d03a82be4 | ||
|
|
aa5fdfa395 | ||
|
|
7e05bcc81d | ||
|
|
e52aa7ad3c | ||
|
|
42faf9bda2 | ||
|
|
984164ba60 | ||
|
|
d26faac0e6 | ||
|
|
b54a9ace9f | ||
|
|
ad0131de66 | ||
|
|
b9ac48ebef | ||
|
|
71751ae4bc | ||
|
|
1c6f4a1d1e | ||
|
|
fd84b3f1c4 | ||
|
|
9d4a052ecc | ||
|
|
f93c8965be | ||
|
|
beef86ce8c | ||
|
|
03b75d5379 | ||
|
|
66855e65bb | ||
|
|
b8da93912f | ||
|
|
d675115615 | ||
|
|
0296a0bde6 | ||
|
|
8409384232 | ||
|
|
cfaee56a08 | ||
|
|
7d65717784 | ||
|
|
7079d50fdf | ||
|
|
41a613e151 | ||
|
|
1f2b6a0acc | ||
|
|
25d402c9d1 | ||
|
|
045b7cf019 | ||
|
|
57109dd72e | ||
|
|
7a8958741d | ||
|
|
f317d74bb4 | ||
|
|
5f0eb91a81 | ||
|
|
66f3a1fe7d | ||
|
|
59d85531ce | ||
|
|
4bd4c3e833 | ||
|
|
47f082e2fc | ||
|
|
1af52d16c0 | ||
|
|
2f52ed3ee0 | ||
|
|
a09407da8e | ||
|
|
1b878b59b8 | ||
|
|
80452ab4cd | ||
|
|
4d9c8e8d3e | ||
|
|
e15bd77789 | ||
|
|
a5f12a50e6 | ||
|
|
07ef06ad95 | ||
|
|
b61836adf7 | ||
|
|
34e2663492 | ||
|
|
8b90f87589 | ||
|
|
8c1e45da6c | ||
|
|
88caff38f0 | ||
|
|
62a6d74393 | ||
|
|
cb94447290 | ||
|
|
e4cf509d0f | ||
|
|
205119cc01 | ||
|
|
f10f98abf2 | ||
|
|
3a7391fbb3 | ||
|
|
2155734e23 | ||
|
|
6806e66d64 | ||
|
|
db8305b5e9 | ||
|
|
e8330eedc3 | ||
|
|
c01c4a860c | ||
|
|
6e778f209d | ||
|
|
b760361184 | ||
|
|
7150befa54 | ||
|
|
5bf0b275cb | ||
|
|
c86bf1d68b | ||
|
|
e61bde1415 | ||
|
|
e4b3f0ddef | ||
|
|
ec73294900 | ||
|
|
30eb949ce1 | ||
|
|
f5d69a3df6 | ||
|
|
3cc39489bd | ||
|
|
ace5d7de9e | ||
|
|
1682eb3915 | ||
|
|
6fd1a990ae | ||
|
|
cfa36a21b5 | ||
|
|
95163d4864 | ||
|
|
5a9645829d | ||
|
|
be78d93c1f | ||
|
|
ac63558645 | ||
|
|
88fb2e49dc | ||
|
|
6e96ad0f17 | ||
|
|
e54754d04f | ||
|
|
e4b2ebe2a4 | ||
|
|
0028431dde | ||
|
|
91d94de1d2 | ||
|
|
0c131ec990 | ||
|
|
54280d5572 | ||
|
|
6d3640a8b0 | ||
|
|
8226073506 | ||
|
|
f4a5e18c69 | ||
|
|
133af93548 | ||
|
|
3546a5d392 | ||
|
|
fb921e951e | ||
|
|
22685d8e3b | ||
|
|
b2d050d136 | ||
|
|
e3ff1dcd96 | ||
|
|
897890e1b4 | ||
|
|
2c95ea00e8 | ||
|
|
00d6ed7dbb | ||
|
|
b23133c79c | ||
|
|
eef2f26707 | ||
|
|
7483ad1732 | ||
|
|
188237db24 | ||
|
|
134624967b | ||
|
|
a1b8d1cd84 | ||
|
|
e7b9293f3b | ||
|
|
93e4a8931d | ||
|
|
c0713eaeda | ||
|
|
000afa1ed6 | ||
|
|
323e25951f | ||
|
|
49d0c0de87 | ||
|
|
e31a835c4e | ||
|
|
dedf5094c1 | ||
|
|
fed4619a92 | ||
|
|
9eb1a20b3f | ||
|
|
ac21b9cc20 | ||
|
|
4a486e3bf8 | ||
|
|
269374e6bb | ||
|
|
e4d97e4059 | ||
|
|
ba567d81cb | ||
|
|
4fb6005f44 | ||
|
|
69ec4175eb | ||
|
|
d46e90dcdb | ||
|
|
900e91e101 | ||
|
|
05d7e33d86 | ||
|
|
7f0aff8c03 | ||
|
|
512e59193d | ||
|
|
8f056e4a82 | ||
|
|
d06a986293 | ||
|
|
d866847c0d | ||
|
|
83472bc354 | ||
|
|
ac784f8905 | ||
|
|
6035281ca1 | ||
|
|
ce8168d97a | ||
|
|
27670525a3 | ||
|
|
4796d7f450 | ||
|
|
79ec96343f | ||
|
|
4572a455c8 | ||
|
|
cb591925ea | ||
|
|
0bc6102096 | ||
|
|
3282a8d76a | ||
|
|
d04befb8d0 | ||
|
|
2c52a4c40c | ||
|
|
f53f71ecf1 | ||
|
|
fc7481c60c | ||
|
|
e59ae9c6e9 | ||
|
|
aae40a7087 | ||
|
|
9f9148fc1f | ||
|
|
20e5832ce3 | ||
|
|
fc29b34573 | ||
|
|
5ea9150af8 | ||
|
|
159a5835e0 | ||
|
|
78d48c8be3 | ||
|
|
9bb6b216e9 | ||
|
|
dc59d1f3ca | ||
|
|
1ab3f53a31 | ||
|
|
fd7d387fb0 | ||
|
|
17a64506f8 | ||
|
|
b68977597b | ||
|
|
2fb9f83227 | ||
|
|
6fd312f784 | ||
|
|
12d59231c5 | ||
|
|
3a7e2c0249 | ||
|
|
62f2defd91 | ||
|
|
9048e07b6b | ||
|
|
0903721a62 | ||
|
|
bf3380755b | ||
|
|
5d327ccd53 | ||
|
|
eb82af9006 | ||
|
|
2cc2902930 | ||
|
|
f4cb0625bc | ||
|
|
edd40ebe9d | ||
|
|
4e8c130cbf | ||
|
|
0760b6a89c | ||
|
|
f34f60b255 | ||
|
|
3c2675fd0d | ||
|
|
f163e348e0 | ||
|
|
71a3d79c82 | ||
|
|
bd1ba67647 | ||
|
|
828688ddd0 | ||
|
|
60cd1320d2 | ||
|
|
a31de89f9c | ||
|
|
a129f53e0c | ||
|
|
6aa86ac2e3 | ||
|
|
28cafbcad2 | ||
|
|
991f60ce44 | ||
|
|
5dbeabcc58 | ||
|
|
b6bcebc588 | ||
|
|
2f3aa57d23 | ||
|
|
a123774bab | ||
|
|
7f56b50267 | ||
|
|
386f0dc142 | ||
|
|
bf90811cef | ||
|
|
72e2c6dca7 | ||
|
|
81fe467b20 | ||
|
|
07cdc6002d | ||
|
|
ee6d17d165 | ||
|
|
6dd1299dff | ||
|
|
d59e358caa | ||
|
|
5e1eb39b87 | ||
|
|
063170463f | ||
|
|
f8a9851bb3 | ||
|
|
b81a30ef12 | ||
|
|
62fb968c9a | ||
|
|
88b8567d2b | ||
|
|
7e4a295ef0 | ||
|
|
289ed85365 | ||
|
|
07dd459f3c | ||
|
|
f104fab593 | ||
|
|
0170f9b44a | ||
|
|
585d96949b | ||
|
|
46b3807f21 | ||
|
|
5e8a73e29d | ||
|
|
796a276b65 | ||
|
|
072290e544 | ||
|
|
5d5b642fa9 | ||
|
|
70761f4ac1 | ||
|
|
9d61f73e22 | ||
|
|
7a3c61a8d0 | ||
|
|
96872d7d41 | ||
|
|
485d6131d5 | ||
|
|
4893e9a58a | ||
|
|
79480c1d73 | ||
|
|
02c015574f | ||
|
|
653872df8e | ||
|
|
248079f041 | ||
|
|
2da756692b | ||
|
|
240a749b87 | ||
|
|
e4324258d3 | ||
|
|
2c33abe5d1 | ||
|
|
09ef1e0319 | ||
|
|
c091d96999 | ||
|
|
1978061a06 | ||
|
|
617370e822 | ||
|
|
0ed6a96781 | ||
|
|
b3c3bf51ba | ||
|
|
0e187fe888 | ||
|
|
43efcf3a99 | ||
|
|
ebc858aed9 | ||
|
|
f94af86ff9 | ||
|
|
da1c58bac6 | ||
|
|
c1c136f6c0 | ||
|
|
8a18119b53 | ||
|
|
777142b992 | ||
|
|
daa54029ed | ||
|
|
136a654639 | ||
|
|
f13e2c0d71 | ||
|
|
97101248a2 | ||
|
|
5150b4b1b6 | ||
|
|
a6d2381a68 | ||
|
|
29161feb49 | ||
|
|
1a4a1c2ccb | ||
|
|
96dac65e31 | ||
|
|
6005282d9f | ||
|
|
129020dbc4 | ||
|
|
54e0242030 | ||
|
|
0e57f4064f | ||
|
|
e50c4528a6 | ||
|
|
342810aa3a | ||
|
|
f84078c7ca | ||
|
|
eba81a6065 | ||
|
|
427e9197d8 | ||
|
|
67d6cf37a5 | ||
|
|
e6451c7ede | ||
|
|
bcd88a1342 | ||
|
|
5ea250d89d | ||
|
|
bdd83a84fd | ||
|
|
ef38855b4b | ||
|
|
9bc8db5a15 | ||
|
|
c53f3843b8 | ||
|
|
56f1f5d47f | ||
|
|
d74ef599d3 | ||
|
|
398c77e277 | ||
|
|
99e562e9e6 | ||
|
|
47bdb8da23 | ||
|
|
869930165c | ||
|
|
d0f052e056 | ||
|
|
afd2325678 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,3 +14,8 @@ project/plugins/project/
|
|||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
.cache
|
.cache
|
||||||
|
.settings
|
||||||
|
|
||||||
|
# IntelliJ specific
|
||||||
|
.idea/
|
||||||
|
.idea_modules/
|
||||||
|
|||||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
202
README.md
202
README.md
@@ -1,25 +1,32 @@
|
|||||||
GitBucket
|
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
GitBucket is a Github clone by Scala, Easy to setup.
|
GitBucket is the easily installable Github clone written with Scala.
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
The current version of GitBucket provides a basic features below:
|
The current version of GitBucket provides a basic features below:
|
||||||
|
|
||||||
- Public / Private Git repository (http access only)
|
- Public / Private Git repository (http and ssh access)
|
||||||
- Repository viewer (some advanced features are not implemented)
|
- Repository viewer and online file editing
|
||||||
|
- Repository search (Code and Issues)
|
||||||
- Wiki
|
- Wiki
|
||||||
- Issues
|
- Issues
|
||||||
|
- Fork / Pull request
|
||||||
|
- Mail notification
|
||||||
|
- Activity timeline
|
||||||
- User management (for Administrators)
|
- User management (for Administrators)
|
||||||
|
- Group (like Organization in Github)
|
||||||
|
- LDAP integration
|
||||||
|
- Gravatar support
|
||||||
|
|
||||||
Following features are not implemented, but we will make them in the future release!
|
Following features are not implemented, but we will make them in the future release!
|
||||||
|
|
||||||
- Fork and pull request
|
- Comment for the changeset
|
||||||
- Timeline
|
|
||||||
- Search
|
|
||||||
- Network graph
|
- Network graph
|
||||||
- Statics
|
- Statistics
|
||||||
- Watch / Star
|
- Watch / Star
|
||||||
- Team management (like Organization in Github)
|
|
||||||
|
|
||||||
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
|
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
|
||||||
|
|
||||||
@@ -27,14 +34,185 @@ Installation
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
|
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
|
||||||
2. Deploy it to the servlet container such as Tomcat or Jetty.
|
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
|
||||||
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
|
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
|
||||||
|
|
||||||
|
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
|
||||||
|
|
||||||
The default administrator account is **root** and password is **root**.
|
The default administrator account is **root** and password is **root**.
|
||||||
|
|
||||||
|
or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
|
||||||
|
|
||||||
|
- --port=[NUMBER]
|
||||||
|
- --prefix=[CONTEXTPATH]
|
||||||
|
- --host=[HOSTNAME]
|
||||||
|
- --gitbucket.home=[DATA_DIR]
|
||||||
|
|
||||||
|
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
|
||||||
|
|
||||||
|
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
|
||||||
|
|
||||||
|
### Mac OS X
|
||||||
|
#### Installing Via Homebrew
|
||||||
|
|
||||||
|
$ brew install gitbucket
|
||||||
|
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
|
||||||
|
######################################################################## 100.0%
|
||||||
|
==> Caveats
|
||||||
|
Note: When using launchctl the port will be 8080.
|
||||||
|
|
||||||
|
To have launchd start gitbucket at login:
|
||||||
|
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
|
||||||
|
Then to load gitbucket now:
|
||||||
|
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
|
||||||
|
Or, if you don't want/need launchctl, you can just run:
|
||||||
|
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
|
||||||
|
==> Summary
|
||||||
|
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
|
||||||
|
|
||||||
|
#### Manual Installation
|
||||||
|
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
|
||||||
|
|
||||||
|
Run the following commands in `Terminal` to
|
||||||
|
|
||||||
|
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
|
||||||
|
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
|
||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
--------
|
--------
|
||||||
|
### 2.2 - 4 Aug 2014
|
||||||
|
- Plug-in system is available
|
||||||
|
- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
|
||||||
|
- tar.gz export for repository contents
|
||||||
|
- LDAP authentication improvement (mail address became optional)
|
||||||
|
- Show news feed of a private repository to members
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.1 - 6 Jul 2014
|
||||||
|
- Upgrade to Slick 2.0 from 1.9
|
||||||
|
- Base part of the plug-in system is merged
|
||||||
|
- Many bug fix and improvements
|
||||||
|
|
||||||
|
### 2.0 - 31 May 2014
|
||||||
|
- Modern Github UI
|
||||||
|
- Preview in AceEditor
|
||||||
|
- Select lines by clicking line number in blob view
|
||||||
|
|
||||||
|
### 1.13 - 29 Apr 2014
|
||||||
|
- Direct file editing in the repository viewer using AceEditor
|
||||||
|
- File attachment for issues
|
||||||
|
- Atom feed of user activity
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.12 - 29 Mar 2014
|
||||||
|
- SSH repository access is available
|
||||||
|
- Allow users can create and management their groups
|
||||||
|
- Git submodule support
|
||||||
|
- Close issues via commit messages
|
||||||
|
- Show repository description below the name on repository page
|
||||||
|
- Fix presentation of the source viewer
|
||||||
|
- Upgrade to sbt 0.13
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.11.1 - 06 Mar 2014
|
||||||
|
- Bug fix
|
||||||
|
|
||||||
|
### 1.11 - 01 Mar 2014
|
||||||
|
- Base URL for redirection, notification and repository URL box is configurable
|
||||||
|
- Remove ```--https``` option because it's possible to substitute in the base url
|
||||||
|
- Headline anchor is available for Markdown contents such as Wiki page
|
||||||
|
- Improve H2 connectivity
|
||||||
|
- Label is available for pull requests not only issues
|
||||||
|
- Delete branch button is added
|
||||||
|
- Repository icons are updated
|
||||||
|
- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
|
||||||
|
- Display reference to issue from others in comment list
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.10 - 01 Feb 2014
|
||||||
|
- Rename repository
|
||||||
|
- Transfer repository owner
|
||||||
|
- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
|
||||||
|
- Add LDAP display name attribute
|
||||||
|
- Response performance improvement
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.9 - 28 Dec 2013
|
||||||
|
- Display GITBUCKET_HOME on the system settings page
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.8 - 30 Nov 2013
|
||||||
|
- Add user and group deletion
|
||||||
|
- Improve pull request performance
|
||||||
|
- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
|
||||||
|
- LDAP StartTLS support
|
||||||
|
- Enable hard wrapping in Markdown
|
||||||
|
- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.7 - 26 Oct 2013
|
||||||
|
- Support working on Java6 in embedded Jetty mode
|
||||||
|
- Add `--host` option to bind specified host name in embedded Jetty mode
|
||||||
|
- Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
|
||||||
|
- Add full name as user property
|
||||||
|
- Change link color for absent Wiki pages
|
||||||
|
- Add ZIP download button to the repository viewer tab
|
||||||
|
- Improve ZIP exporting performance
|
||||||
|
- Expand issue and comment textarea for long text automatically
|
||||||
|
- Add conflict detection in Wiki
|
||||||
|
- Add reverting wiki page from history
|
||||||
|
- Match committer to user name by email address
|
||||||
|
- Mail notification sender is customizable
|
||||||
|
- Add link to changeset in refs comment for issues
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.6 - 1 Oct 2013
|
||||||
|
- Web hook
|
||||||
|
- Performance improvement for pull request
|
||||||
|
- Executable war file
|
||||||
|
- Specify suitable Content-Type for downloaded files in the repository viewer
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.5 - 4 Sep 2013
|
||||||
|
- Fork and pull request
|
||||||
|
- LDAP authentication
|
||||||
|
- Mail notification
|
||||||
|
- Add an option to turn off the gravatar support
|
||||||
|
- Add the branch tab in the repository viewer
|
||||||
|
- Encoding auto detection for the file content in the repository viewer
|
||||||
|
- Add favicon, header logo and icons for the timeline
|
||||||
|
- Specify data directory via environment variable GITBUCKET_HOME
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.4 - 31 Jul 2013
|
||||||
|
- Group management
|
||||||
|
- Repository search for code and issues
|
||||||
|
- Display user related issues on the dashboard
|
||||||
|
- Display participants avatar of issues on the issue page
|
||||||
|
- Performance improvement for repository viewer
|
||||||
|
- Alert by milestone due date
|
||||||
|
- H2 database administration console
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.3 - 18 Jul 2013
|
||||||
|
- Batch updating for issues
|
||||||
|
- Display assigned user on issue list
|
||||||
|
- User icon and Gravatar support
|
||||||
|
- Convert @xxxx to link to the account page
|
||||||
|
- Add copy to clipboard button for git clone URL
|
||||||
|
- Allow multi-byte characters as wiki page name
|
||||||
|
- Allow to create the empty repository
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.2 - 09 Jul 2013
|
||||||
|
- Add activity timeline
|
||||||
|
- Bugfix for Git 1.8.1.5 or later
|
||||||
|
- Allow multi-byte characters as label
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### 1.1 - 05 Jul 2013
|
||||||
|
- Fix some bugs
|
||||||
|
- Upgrade to JGit 3.0
|
||||||
|
|
||||||
### 1.0 - 04 Jul 2013
|
### 1.0 - 04 Jul 2013
|
||||||
|
- This is a first public release
|
||||||
- This is a first public release.
|
|
||||||
|
|||||||
61
build.xml
Normal file
61
build.xml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<project name="gitbucket" default="all" basedir=".">
|
||||||
|
|
||||||
|
<property name="target.dir" value="target"/>
|
||||||
|
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
|
||||||
|
<property name="jetty.dir" value="embed-jetty"/>
|
||||||
|
<property name="scala.version" value="2.11"/>
|
||||||
|
<property name="gitbucket.version" value="0.0.1"/>
|
||||||
|
<property name="jetty.version" value="8.1.8.v20121106"/>
|
||||||
|
<property name="servlet.version" value="3.0.0.v201112011016"/>
|
||||||
|
|
||||||
|
<condition property="sbt.exec" value="sbt.bat" else="sbt.sh">
|
||||||
|
<os family="windows" />
|
||||||
|
</condition>
|
||||||
|
|
||||||
|
<target name="clean">
|
||||||
|
<delete dir="${embed.classes.dir}"/>
|
||||||
|
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="war" depends="clean">
|
||||||
|
<exec executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
|
||||||
|
<arg line="clean compile test package" />
|
||||||
|
</exec>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="embed" depends="war">
|
||||||
|
<mkdir dir="${embed.classes.dir}"/>
|
||||||
|
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
|
||||||
|
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
|
||||||
|
|
||||||
|
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||||
|
basedir="${embed.classes.dir}"
|
||||||
|
update = "true"
|
||||||
|
includes="javax/**,org/**"/>
|
||||||
|
|
||||||
|
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||||
|
basedir="${target.dir}/scala-${scala.version}/classes"
|
||||||
|
update = "true"
|
||||||
|
includes="JettyLauncher.class,HttpsSupportConnector.class"/>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="rename" depends="embed">
|
||||||
|
<move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||||
|
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="all" depends="rename">
|
||||||
|
</target>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
||||||
20
contrib/macosx/gitbucket.plist
Normal file
20
contrib/macosx/gitbucket.plist
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>gitbucket</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/bin/java</string>
|
||||||
|
<string>-Dmail.smtp.starttls.enable=true</string>
|
||||||
|
<string>-jar</string>
|
||||||
|
<string>gitbucket.war</string>
|
||||||
|
<string>--host=127.0.0.1</string>
|
||||||
|
<string>--port=8080</string>
|
||||||
|
<string>--https=true</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
17
contrib/redhat/gitbucket.conf
Normal file
17
contrib/redhat/gitbucket.conf
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Bind host
|
||||||
|
#GITBUCKET_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Server port
|
||||||
|
#GITBUCKET_PORT=8080
|
||||||
|
|
||||||
|
# Data directory (GITBUCKET_HOME/gitbucket)
|
||||||
|
#GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
|
||||||
|
# Path to the WAR file
|
||||||
|
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
|
||||||
|
#GITBUCKET_PREFIX=
|
||||||
|
|
||||||
|
# Other Java option
|
||||||
|
#GITBUCKET_JVM_OPTS=
|
||||||
106
contrib/redhat/gitbucket.init
Normal file
106
contrib/redhat/gitbucket.init
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# /etc/rc.d/init.d/gitbucket
|
||||||
|
#
|
||||||
|
# Starts the GitBucket server
|
||||||
|
#
|
||||||
|
# chkconfig: 345 60 40
|
||||||
|
# description: Run GitBucket server
|
||||||
|
# processname: java
|
||||||
|
|
||||||
|
# Source function library
|
||||||
|
. /etc/rc.d/init.d/functions
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# Pull in cq settings
|
||||||
|
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket
|
||||||
|
|
||||||
|
# Location of the log and PID file
|
||||||
|
LOG_FILE=/var/log/gitbucket/run.log
|
||||||
|
PID_FILE=/var/run/gitbucket.pid
|
||||||
|
|
||||||
|
# Default return value
|
||||||
|
RETVAL=0
|
||||||
|
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo -n $"Starting GitBucket server: "
|
||||||
|
|
||||||
|
# Compile statup parameters
|
||||||
|
if [ $GITBUCKET_PORT ]; then
|
||||||
|
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_PREFIX ]; then
|
||||||
|
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_HOST ]; then
|
||||||
|
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the Java process
|
||||||
|
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
# Store PID of the Java process into a file
|
||||||
|
echo $! > $PID_FILE
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "GitBucket startup"
|
||||||
|
else
|
||||||
|
failure "GitBucket startup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -n $"Stopping GitBucket server: "
|
||||||
|
|
||||||
|
# Run the Java process
|
||||||
|
kill $(cat $PID_FILE 2>/dev/null) >>$LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
rm -f $PID_FILE
|
||||||
|
success "GitBucket stopping"
|
||||||
|
else
|
||||||
|
failure "GitBucket stopping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status -p $PID_FILE java
|
||||||
|
RETVAL=$?
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo $"Usage: $0 [start|stop|restart|status]"
|
||||||
|
RETVAL=2
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
exit $RETVAL
|
||||||
47
contrib/redhat/gitbucket.spec
Normal file
47
contrib/redhat/gitbucket.spec
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
Name: gitbucket
|
||||||
|
Summary: GitHub clone written with Scala.
|
||||||
|
Version: 1.7
|
||||||
|
Release: 1%{?dist}
|
||||||
|
License: Apache
|
||||||
|
URL: https://github.com/takezoe/gitbucket
|
||||||
|
Group: System/Servers
|
||||||
|
Source0: %{name}.war
|
||||||
|
Source1: %{name}.init
|
||||||
|
Source2: %{name}.conf
|
||||||
|
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||||
|
BuildArch: noarch
|
||||||
|
Requires: java >= 1.7
|
||||||
|
|
||||||
|
|
||||||
|
%description
|
||||||
|
|
||||||
|
GitBucket is the easily installable GitHub clone written with Scala.
|
||||||
|
|
||||||
|
|
||||||
|
%install
|
||||||
|
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
||||||
|
%{__mkdir_p} %{buildroot}{%{_sysconfdir}/{init.d,sysconfig},%{_datarootdir}/%{name}/lib,%{_sharedstatedir}/%{name},%{_localstatedir}/log/%{name}}
|
||||||
|
%{__install} -m 0644 %{SOURCE0} %{buildroot}%{_datarootdir}/%{name}/lib
|
||||||
|
%{__install} -m 0755 %{SOURCE1} %{buildroot}%{_sysconfdir}/init.d/%{name}
|
||||||
|
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
|
||||||
|
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
||||||
|
|
||||||
|
|
||||||
|
%clean
|
||||||
|
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
||||||
|
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(-,root,root,-)
|
||||||
|
%{_datarootdir}/%{name}/lib/%{name}.war
|
||||||
|
%{_sysconfdir}/init.d/%{name}
|
||||||
|
%config %{_sysconfdir}/sysconfig/%{name}
|
||||||
|
%{_localstatedir}/log/%{name}/run.log
|
||||||
|
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||||
|
- Version bump to v1.7.
|
||||||
|
|
||||||
|
* Thu Oct 17 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||||
|
- First build.
|
||||||
BIN
embed-jetty/javax.servlet-3.0.0.v201112011016.jar
Normal file
BIN
embed-jetty/javax.servlet-3.0.0.v201112011016.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-continuation-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-continuation-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-http-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-http-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-io-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-io-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-security-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-security-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-server-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-server-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-servlet-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-servlet-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-util-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-util-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-webapp-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-webapp-8.1.8.v20121106.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-xml-8.1.8.v20121106.jar
Normal file
BIN
embed-jetty/jetty-xml-8.1.8.v20121106.jar
Normal file
Binary file not shown.
@@ -23,8 +23,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>37</x>
|
<x>33</x>
|
||||||
<y>36</y>
|
<y>18</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections/>
|
<sourceConnections/>
|
||||||
<targetConnections>
|
<targetConnections>
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>751</x>
|
<x>723</x>
|
||||||
<y>47</y>
|
<y>138</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections>
|
<sourceConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>882</x>
|
<x>1182</x>
|
||||||
<y>239</y>
|
<y>339</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections/>
|
<sourceConnections/>
|
||||||
<targetConnections>
|
<targetConnections>
|
||||||
@@ -108,8 +108,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>940</x>
|
<x>1301</x>
|
||||||
<y>615</y>
|
<y>836</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections>
|
<sourceConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||||
@@ -138,8 +138,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>420</x>
|
<x>684</x>
|
||||||
<y>758</y>
|
<y>858</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections>
|
<sourceConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||||
@@ -167,8 +167,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>307</x>
|
<x>293</x>
|
||||||
<y>356</y>
|
<y>478</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections>
|
<sourceConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
@@ -210,8 +210,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>641</x>
|
<x>875</x>
|
||||||
<y>569</y>
|
<y>677</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections>
|
<sourceConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
@@ -283,9 +283,14 @@
|
|||||||
<defaultValue></defaultValue>
|
<defaultValue></defaultValue>
|
||||||
</net.java.amateras.db.visual.model.ColumnModel>
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>MILESTONE_NAME</columnName>
|
<columnName>TITLE</columnName>
|
||||||
<logicalName>Milestone Name</logicalName>
|
<logicalName>Title</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel/columnType"/>
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
|
<name>VARCHAR</name>
|
||||||
|
<logicalName>文字列</logicalName>
|
||||||
|
<supportSize>true</supportSize>
|
||||||
|
<type>12</type>
|
||||||
|
</columnType>
|
||||||
<size>100</size>
|
<size>100</size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
@@ -293,6 +298,49 @@
|
|||||||
<autoIncrement>false</autoIncrement>
|
<autoIncrement>false</autoIncrement>
|
||||||
<defaultValue></defaultValue>
|
<defaultValue></defaultValue>
|
||||||
</net.java.amateras.db.visual.model.ColumnModel>
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>DESCRIPTION</columnName>
|
||||||
|
<logicalName>Description</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
|
<name>TEXT</name>
|
||||||
|
<logicalName>文字列</logicalName>
|
||||||
|
<supportSize>true</supportSize>
|
||||||
|
<type>2005</type>
|
||||||
|
</columnType>
|
||||||
|
<size></size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>DUE_DATE</columnName>
|
||||||
|
<logicalName>Due Date</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
|
<name>TIMESTAMP</name>
|
||||||
|
<logicalName>日時</logicalName>
|
||||||
|
<supportSize>false</supportSize>
|
||||||
|
<type>93</type>
|
||||||
|
</columnType>
|
||||||
|
<size>10</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>CLOSED_DATE</columnName>
|
||||||
|
<logicalName>Closed Date</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
||||||
|
<size>10</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
</columns>
|
</columns>
|
||||||
<indices/>
|
<indices/>
|
||||||
<backgroundColor>
|
<backgroundColor>
|
||||||
@@ -350,6 +398,36 @@
|
|||||||
</entry>
|
</entry>
|
||||||
</references>
|
</references>
|
||||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||||
|
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../../../../../../../.."/>
|
||||||
|
<foreignKeyName>ISSUE_FK_2</foreignKeyName>
|
||||||
|
<references>
|
||||||
|
<entry>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ASSIGNED_USER_NAME</columnName>
|
||||||
|
<logicalName>Assinged User Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
</entry>
|
||||||
|
</references>
|
||||||
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
</sourceConnections>
|
</sourceConnections>
|
||||||
<targetConnections>
|
<targetConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
@@ -375,8 +453,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>26</x>
|
<x>18</x>
|
||||||
<y>660</y>
|
<y>776</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections>
|
<sourceConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||||
@@ -462,6 +540,22 @@
|
|||||||
<autoIncrement>true</autoIncrement>
|
<autoIncrement>true</autoIncrement>
|
||||||
<defaultValue></defaultValue>
|
<defaultValue></defaultValue>
|
||||||
</net.java.amateras.db.visual.model.ColumnModel>
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ACTION</columnName>
|
||||||
|
<logicalName>Action</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
|
<name>VARCHAR</name>
|
||||||
|
<logicalName>文字列</logicalName>
|
||||||
|
<supportSize>true</supportSize>
|
||||||
|
<type>12</type>
|
||||||
|
</columnType>
|
||||||
|
<size>20</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description>Expand to VARCHAR(20) from VARCHAR(10) in 1.3</description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
|
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
|
||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>CONTENT</columnName>
|
<columnName>CONTENT</columnName>
|
||||||
@@ -498,7 +592,7 @@
|
|||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>UPDATED_DATE</columnName>
|
<columnName>UPDATED_DATE</columnName>
|
||||||
<logicalName>Updated Date</logicalName>
|
<logicalName>Updated Date</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
|
||||||
<size>10</size>
|
<size>10</size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
@@ -572,10 +666,11 @@
|
|||||||
<autoIncrement>false</autoIncrement>
|
<autoIncrement>false</autoIncrement>
|
||||||
<defaultValue></defaultValue>
|
<defaultValue></defaultValue>
|
||||||
</net.java.amateras.db.visual.model.ColumnModel>
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
|
||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>TITLE</columnName>
|
<columnName>TITLE</columnName>
|
||||||
<logicalName>Title</logicalName>
|
<logicalName>Title</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
||||||
<size></size>
|
<size></size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
@@ -586,7 +681,7 @@
|
|||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>CONTENT</columnName>
|
<columnName>CONTENT</columnName>
|
||||||
<logicalName>Content</logicalName>
|
<logicalName>Content</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
||||||
<size></size>
|
<size></size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
@@ -597,7 +692,7 @@
|
|||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>REGISTERED_DATE</columnName>
|
<columnName>REGISTERED_DATE</columnName>
|
||||||
<logicalName>Registered Date</logicalName>
|
<logicalName>Registered Date</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
|
||||||
<size>10</size>
|
<size>10</size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
@@ -608,7 +703,7 @@
|
|||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>UPDATED_DATE</columnName>
|
<columnName>UPDATED_DATE</columnName>
|
||||||
<logicalName>Updated Date</logicalName>
|
<logicalName>Updated Date</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
|
||||||
<size>10</size>
|
<size>10</size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
@@ -801,8 +896,8 @@
|
|||||||
<constraint>
|
<constraint>
|
||||||
<height>-1</height>
|
<height>-1</height>
|
||||||
<width>-1</width>
|
<width>-1</width>
|
||||||
<x>388</x>
|
<x>481</x>
|
||||||
<y>166</y>
|
<y>361</y>
|
||||||
</constraint>
|
</constraint>
|
||||||
<sourceConnections>
|
<sourceConnections>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||||
@@ -862,6 +957,250 @@
|
|||||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel">
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<constraint>
|
||||||
|
<height>-1</height>
|
||||||
|
<width>-1</width>
|
||||||
|
<x>1199</x>
|
||||||
|
<y>25</y>
|
||||||
|
</constraint>
|
||||||
|
<sourceConnections>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||||
|
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../.."/>
|
||||||
|
<foreignKeyName>ACTIVITY_FK_2</foreignKeyName>
|
||||||
|
<references>
|
||||||
|
<entry>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ACTIVITY_USER_NAME</columnName>
|
||||||
|
<logicalName>Activity User Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
</entry>
|
||||||
|
</references>
|
||||||
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
</sourceConnections>
|
||||||
|
<targetConnections/>
|
||||||
|
<error></error>
|
||||||
|
<linkedPath></linkedPath>
|
||||||
|
<tableName>ACTIVITY</tableName>
|
||||||
|
<logicalName>Activity</logicalName>
|
||||||
|
<description>Since 1.2</description>
|
||||||
|
<columns>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ACTIVITY_ID</columnName>
|
||||||
|
<logicalName>Activity ID</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
|
<name>INT</name>
|
||||||
|
<logicalName>整数</logicalName>
|
||||||
|
<supportSize>false</supportSize>
|
||||||
|
<type>4</type>
|
||||||
|
</columnType>
|
||||||
|
<size>10</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>true</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>true</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>USER_NAME</columnName>
|
||||||
|
<logicalName>User Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>REPOSITORY_NAME</columnName>
|
||||||
|
<logicalName>Repository Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ACTIVITY_TYPE</columnName>
|
||||||
|
<logicalName>Activity Type</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>MESSAGE</columnName>
|
||||||
|
<logicalName>Message</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||||
|
<size></size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ADDITIONAL_INFO</columnName>
|
||||||
|
<logicalName>Additional Information</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||||
|
<size></size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ACTIVITY_DATE</columnName>
|
||||||
|
<logicalName>Activity Date</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
||||||
|
<size>10</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
</columns>
|
||||||
|
<indices/>
|
||||||
|
<backgroundColor>
|
||||||
|
<red>255</red>
|
||||||
|
<green>255</green>
|
||||||
|
<blue>206</blue>
|
||||||
|
</backgroundColor>
|
||||||
|
<sql></sql>
|
||||||
|
</source>
|
||||||
|
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||||
|
<foreignKeyName>ACTIVITY_FK_1</foreignKeyName>
|
||||||
|
<references/>
|
||||||
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel">
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<constraint>
|
||||||
|
<height>-1</height>
|
||||||
|
<width>-1</width>
|
||||||
|
<x>1451</x>
|
||||||
|
<y>577</y>
|
||||||
|
</constraint>
|
||||||
|
<sourceConnections>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||||
|
</sourceConnections>
|
||||||
|
<targetConnections/>
|
||||||
|
<error></error>
|
||||||
|
<linkedPath></linkedPath>
|
||||||
|
<tableName>COMMIT_LOG</tableName>
|
||||||
|
<logicalName>Commit Log</logicalName>
|
||||||
|
<description>Since 1.2</description>
|
||||||
|
<columns>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>USER_NAME</columnName>
|
||||||
|
<logicalName>User Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>true</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>REPOSITORY_NAME</columnName>
|
||||||
|
<logicalName>Repository Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>true</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>COMMIT_ID</columnName>
|
||||||
|
<logicalName>Commit ID</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>40</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>true</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
</columns>
|
||||||
|
<indices/>
|
||||||
|
<backgroundColor>
|
||||||
|
<red>255</red>
|
||||||
|
<green>255</green>
|
||||||
|
<blue>206</blue>
|
||||||
|
</backgroundColor>
|
||||||
|
<sql></sql>
|
||||||
|
</source>
|
||||||
|
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||||
|
<foreignKeyName>COMMIT_LOG_FK_1</foreignKeyName>
|
||||||
|
<references/>
|
||||||
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
</targetConnections>
|
</targetConnections>
|
||||||
<error></error>
|
<error></error>
|
||||||
<linkedPath></linkedPath>
|
<linkedPath></linkedPath>
|
||||||
@@ -1062,6 +1401,100 @@
|
|||||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]"/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]"/>
|
||||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]"/>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel">
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<constraint>
|
||||||
|
<height>-1</height>
|
||||||
|
<width>-1</width>
|
||||||
|
<x>432</x>
|
||||||
|
<y>240</y>
|
||||||
|
</constraint>
|
||||||
|
<sourceConnections>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||||
|
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../.."/>
|
||||||
|
<foreignKeyName>GROUP_MEMBER_FK_2</foreignKeyName>
|
||||||
|
<references/>
|
||||||
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
</sourceConnections>
|
||||||
|
<targetConnections/>
|
||||||
|
<error></error>
|
||||||
|
<linkedPath></linkedPath>
|
||||||
|
<tableName>GROUP_MEMBER</tableName>
|
||||||
|
<logicalName>Group Member</logicalName>
|
||||||
|
<description>Since 1.4</description>
|
||||||
|
<columns>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>GROUP_NAME</columnName>
|
||||||
|
<logicalName>Group Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>true</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>USER_NAME</columnName>
|
||||||
|
<logicalName>User Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>true</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
</columns>
|
||||||
|
<indices/>
|
||||||
|
<backgroundColor>
|
||||||
|
<red>255</red>
|
||||||
|
<green>255</green>
|
||||||
|
<blue>206</blue>
|
||||||
|
</backgroundColor>
|
||||||
|
<sql></sql>
|
||||||
|
</source>
|
||||||
|
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||||
|
<foreignKeyName>GROUP_MEMBER_FK_1</foreignKeyName>
|
||||||
|
<references>
|
||||||
|
<entry>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel reference="../../../source/columns/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||||
|
</entry>
|
||||||
|
</references>
|
||||||
|
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||||
|
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
|
||||||
</targetConnections>
|
</targetConnections>
|
||||||
<error></error>
|
<error></error>
|
||||||
<linkedPath></linkedPath>
|
<linkedPath></linkedPath>
|
||||||
@@ -1089,8 +1522,8 @@
|
|||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>PASSWORD</columnName>
|
<columnName>PASSWORD</columnName>
|
||||||
<logicalName>Password</logicalName>
|
<logicalName>Password</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[2]/columnType"/>
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||||
<size>20</size>
|
<size>40</size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
<description></description>
|
<description></description>
|
||||||
@@ -1098,18 +1531,18 @@
|
|||||||
<defaultValue></defaultValue>
|
<defaultValue></defaultValue>
|
||||||
</net.java.amateras.db.visual.model.ColumnModel>
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<net.java.amateras.db.visual.model.ColumnModel>
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
<columnName>USER_TYPE</columnName>
|
<columnName>ADMINISTRATOR</columnName>
|
||||||
<logicalName>User Type</logicalName>
|
<logicalName>Administrator</logicalName>
|
||||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
<name>INT</name>
|
<name>BOOLEAN</name>
|
||||||
<logicalName>整数</logicalName>
|
<logicalName>真偽値</logicalName>
|
||||||
<supportSize>false</supportSize>
|
<supportSize>false</supportSize>
|
||||||
<type>4</type>
|
<type>16</type>
|
||||||
</columnType>
|
</columnType>
|
||||||
<size>10</size>
|
<size>10</size>
|
||||||
<notNull>true</notNull>
|
<notNull>true</notNull>
|
||||||
<primaryKey>false</primaryKey>
|
<primaryKey>false</primaryKey>
|
||||||
<description>0:Normal 1:Administrator</description>
|
<description></description>
|
||||||
<autoIncrement>false</autoIncrement>
|
<autoIncrement>false</autoIncrement>
|
||||||
<defaultValue>0</defaultValue>
|
<defaultValue>0</defaultValue>
|
||||||
</net.java.amateras.db.visual.model.ColumnModel>
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
@@ -1157,6 +1590,33 @@
|
|||||||
<autoIncrement>false</autoIncrement>
|
<autoIncrement>false</autoIncrement>
|
||||||
<defaultValue></defaultValue>
|
<defaultValue></defaultValue>
|
||||||
</net.java.amateras.db.visual.model.ColumnModel>
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>IMAGE</columnName>
|
||||||
|
<logicalName>Image</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description>Since 1.3</description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>GROUP_ACCOUNT</columnName>
|
||||||
|
<logicalName>Group Account</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
|
<name>BOOLEAN</name>
|
||||||
|
<logicalName>真偽値</logicalName>
|
||||||
|
<supportSize>false</supportSize>
|
||||||
|
<type>16</type>
|
||||||
|
</columnType>
|
||||||
|
<size>10</size>
|
||||||
|
<notNull>true</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description>Since 1.4</description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue>FALSE</defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
</columns>
|
</columns>
|
||||||
<indices>
|
<indices>
|
||||||
<net.java.amateras.db.visual.model.IndexModel>
|
<net.java.amateras.db.visual.model.IndexModel>
|
||||||
@@ -1184,6 +1644,91 @@
|
|||||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target"/>
|
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target"/>
|
||||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source"/>
|
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source"/>
|
||||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/source"/>
|
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/source"/>
|
||||||
|
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
|
||||||
|
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[7]/source"/>
|
||||||
|
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
|
||||||
|
<net.java.amateras.db.visual.model.TableModel>
|
||||||
|
<listeners serialization="custom">
|
||||||
|
<java.beans.PropertyChangeSupport>
|
||||||
|
<default>
|
||||||
|
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||||
|
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||||
|
</default>
|
||||||
|
<null/>
|
||||||
|
</java.beans.PropertyChangeSupport>
|
||||||
|
</listeners>
|
||||||
|
<constraint>
|
||||||
|
<height>-1</height>
|
||||||
|
<width>-1</width>
|
||||||
|
<x>410</x>
|
||||||
|
<y>860</y>
|
||||||
|
</constraint>
|
||||||
|
<sourceConnections/>
|
||||||
|
<targetConnections/>
|
||||||
|
<error></error>
|
||||||
|
<linkedPath></linkedPath>
|
||||||
|
<tableName>ISSUE_OUTLINE_VIEW</tableName>
|
||||||
|
<logicalName>Issue Outline View</logicalName>
|
||||||
|
<description>Since 1.4</description>
|
||||||
|
<columns>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>USER_NAME</columnName>
|
||||||
|
<logicalName>User Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>REPOSITORY_NAME</columnName>
|
||||||
|
<logicalName>Repository Name</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||||
|
<size>100</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>ISSUE_ID</columnName>
|
||||||
|
<logicalName>Issue ID</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||||
|
<name>INT</name>
|
||||||
|
<logicalName>整数</logicalName>
|
||||||
|
<supportSize>false</supportSize>
|
||||||
|
<type>4</type>
|
||||||
|
</columnType>
|
||||||
|
<size>10</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
<columnName>COMMENT_COUNT</columnName>
|
||||||
|
<logicalName>Comment Count</logicalName>
|
||||||
|
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[3]/columnType"/>
|
||||||
|
<size>10</size>
|
||||||
|
<notNull>false</notNull>
|
||||||
|
<primaryKey>false</primaryKey>
|
||||||
|
<description></description>
|
||||||
|
<autoIncrement>false</autoIncrement>
|
||||||
|
<defaultValue></defaultValue>
|
||||||
|
</net.java.amateras.db.visual.model.ColumnModel>
|
||||||
|
</columns>
|
||||||
|
<indices/>
|
||||||
|
<backgroundColor>
|
||||||
|
<red>210</red>
|
||||||
|
<green>232</green>
|
||||||
|
<blue>249</blue>
|
||||||
|
</backgroundColor>
|
||||||
|
<sql></sql>
|
||||||
|
</net.java.amateras.db.visual.model.TableModel>
|
||||||
</children>
|
</children>
|
||||||
<dommains/>
|
<dommains/>
|
||||||
<dialectName>H2</dialectName>
|
<dialectName>H2</dialectName>
|
||||||
1703
etc/icons.svg
Normal file
1703
etc/icons.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sbt.version=0.13.1
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import sbt._
|
import sbt._
|
||||||
import Keys._
|
import Keys._
|
||||||
import org.scalatra.sbt._
|
import org.scalatra.sbt._
|
||||||
import org.scalatra.sbt.PluginKeys._
|
|
||||||
import twirl.sbt.TwirlPlugin._
|
import twirl.sbt.TwirlPlugin._
|
||||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
||||||
|
|
||||||
@@ -9,35 +8,51 @@ object MyBuild extends Build {
|
|||||||
val Organization = "jp.sf.amateras"
|
val Organization = "jp.sf.amateras"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val Version = "0.0.1"
|
val Version = "0.0.1"
|
||||||
val ScalaVersion = "2.10.1"
|
val ScalaVersion = "2.11.2"
|
||||||
val ScalatraVersion = "2.2.0"
|
val ScalatraVersion = "2.3.0"
|
||||||
|
|
||||||
lazy val project = Project (
|
lazy val project = Project (
|
||||||
"gitbucket",
|
"gitbucket",
|
||||||
file("."),
|
file("."),
|
||||||
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
|
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
|
||||||
|
sourcesInBase := false,
|
||||||
organization := Organization,
|
organization := Organization,
|
||||||
name := Name,
|
name := Name,
|
||||||
version := Version,
|
version := Version,
|
||||||
scalaVersion := ScalaVersion,
|
scalaVersion := ScalaVersion,
|
||||||
resolvers += Classpaths.typesafeReleases,
|
resolvers ++= Seq(
|
||||||
|
Classpaths.typesafeReleases,
|
||||||
|
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||||
|
),
|
||||||
|
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "2.3.1.201302201838-r",
|
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
|
||||||
"org.apache.commons" % "commons-io" % "1.3.2",
|
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
|
||||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||||
"org.json4s" %% "json4s-jackson" % "3.2.4",
|
"org.json4s" %% "json4s-jackson" % "3.2.10",
|
||||||
|
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
|
||||||
"commons-io" % "commons-io" % "2.4",
|
"commons-io" % "commons-io" % "2.4",
|
||||||
"org.pegdown" % "pegdown" % "1.3.0",
|
"org.pegdown" % "pegdown" % "1.4.1",
|
||||||
"org.apache.commons" % "commons-compress" % "1.5",
|
"org.apache.commons" % "commons-compress" % "1.5",
|
||||||
"com.typesafe.slick" %% "slick" % "1.0.1",
|
"org.apache.commons" % "commons-email" % "1.3.1",
|
||||||
"com.h2database" % "h2" % "1.3.171",
|
"org.apache.httpcomponents" % "httpclient" % "4.3",
|
||||||
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
|
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
|
"com.typesafe.slick" %% "slick" % "2.1.0-RC3",
|
||||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
|
"org.mozilla" % "rhino" % "1.7R4",
|
||||||
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
|
"org.quartz-scheduler" % "quartz" % "2.2.1",
|
||||||
|
"com.h2database" % "h2" % "1.4.180",
|
||||||
|
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||||
|
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
||||||
|
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||||
|
"junit" % "junit" % "4.11" % "test"
|
||||||
),
|
),
|
||||||
EclipseKeys.withSource := true
|
EclipseKeys.withSource := true,
|
||||||
|
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
|
||||||
|
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
|
||||||
|
packageOptions += Package.MainClass("JettyLauncher")
|
||||||
) ++ seq(Twirl.settings: _*)
|
) ++ seq(Twirl.settings: _*)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.2")
|
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
|
||||||
|
|
||||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0")
|
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
|
||||||
|
|
||||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.2.0")
|
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
|
||||||
|
|
||||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
|
resolvers += "spray repo" at "http://repo.spray.io"
|
||||||
|
|
||||||
|
addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
|
||||||
|
|
||||||
|
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
|
||||||
|
|||||||
Binary file not shown.
BIN
sbt-launch-0.13.1.jar
Normal file
BIN
sbt-launch-0.13.1.jar
Normal file
Binary file not shown.
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
|||||||
set SCRIPT_DIR=%~dp0
|
set SCRIPT_DIR=%~dp0
|
||||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
|
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %*
|
||||||
|
|||||||
1
sbt.sh
Executable file
1
sbt.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.1.jar "$@"
|
||||||
62
src/main/java/JettyLauncher.java
Normal file
62
src/main/java/JettyLauncher.java
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import org.eclipse.jetty.io.EndPoint;
|
||||||
|
import org.eclipse.jetty.server.Request;
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||||
|
import org.eclipse.jetty.webapp.WebAppContext;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.ProtectionDomain;
|
||||||
|
|
||||||
|
public class JettyLauncher {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
String host = null;
|
||||||
|
int port = 8080;
|
||||||
|
String contextPath = "/";
|
||||||
|
boolean forceHttps = false;
|
||||||
|
|
||||||
|
for(String arg: args) {
|
||||||
|
if(arg.startsWith("--") && arg.contains("=")) {
|
||||||
|
String[] dim = arg.split("=");
|
||||||
|
if(dim.length >= 2) {
|
||||||
|
if(dim[0].equals("--host")) {
|
||||||
|
host = dim[1];
|
||||||
|
} else if(dim[0].equals("--port")) {
|
||||||
|
port = Integer.parseInt(dim[1]);
|
||||||
|
} else if(dim[0].equals("--prefix")) {
|
||||||
|
contextPath = dim[1];
|
||||||
|
} else if(dim[0].equals("--gitbucket.home")){
|
||||||
|
System.setProperty("gitbucket.home", dim[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Server server = new Server();
|
||||||
|
|
||||||
|
SelectChannelConnector connector = new SelectChannelConnector();
|
||||||
|
if(host != null) {
|
||||||
|
connector.setHost(host);
|
||||||
|
}
|
||||||
|
connector.setMaxIdleTime(1000 * 60 * 60);
|
||||||
|
connector.setSoLingerTime(-1);
|
||||||
|
connector.setPort(port);
|
||||||
|
server.addConnector(connector);
|
||||||
|
|
||||||
|
WebAppContext context = new WebAppContext();
|
||||||
|
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
|
||||||
|
URL location = domain.getCodeSource().getLocation();
|
||||||
|
|
||||||
|
context.setContextPath(contextPath);
|
||||||
|
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
|
||||||
|
context.setServer(server);
|
||||||
|
context.setWar(location.toExternalForm());
|
||||||
|
if (forceHttps) {
|
||||||
|
context.setInitParameter("org.scalatra.ForceHttps", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
server.setHandler(context);
|
||||||
|
server.start();
|
||||||
|
server.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/main/java/util/PatchUtil.java
Normal file
93
src/main/java/util/PatchUtil.java
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package util;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.api.errors.PatchApplyException;
|
||||||
|
import org.eclipse.jgit.diff.RawText;
|
||||||
|
import org.eclipse.jgit.internal.JGitText;
|
||||||
|
import org.eclipse.jgit.patch.FileHeader;
|
||||||
|
import org.eclipse.jgit.patch.HunkHeader;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class helps to apply patch. Most of these code came from {@link org.eclipse.jgit.api.ApplyCommand}.
|
||||||
|
*/
|
||||||
|
public class PatchUtil {
|
||||||
|
|
||||||
|
public static String apply(String source, String patch, FileHeader fh)
|
||||||
|
throws IOException, PatchApplyException {
|
||||||
|
RawText rt = new RawText(source.getBytes("UTF-8"));
|
||||||
|
List<String> oldLines = new ArrayList<String>(rt.size());
|
||||||
|
for (int i = 0; i < rt.size(); i++)
|
||||||
|
oldLines.add(rt.getString(i));
|
||||||
|
List<String> newLines = new ArrayList<String>(oldLines);
|
||||||
|
for (HunkHeader hh : fh.getHunks()) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset());
|
||||||
|
RawText hrt = new RawText(out.toByteArray());
|
||||||
|
List<String> hunkLines = new ArrayList<String>(hrt.size());
|
||||||
|
for (int i = 0; i < hrt.size(); i++)
|
||||||
|
hunkLines.add(hrt.getString(i));
|
||||||
|
int pos = 0;
|
||||||
|
for (int j = 1; j < hunkLines.size(); j++) {
|
||||||
|
String hunkLine = hunkLines.get(j);
|
||||||
|
switch (hunkLine.charAt(0)) {
|
||||||
|
case ' ':
|
||||||
|
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
|
||||||
|
hunkLine.substring(1))) {
|
||||||
|
throw new PatchApplyException(MessageFormat.format(
|
||||||
|
JGitText.get().patchApplyException, hh));
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
|
||||||
|
hunkLine.substring(1))) {
|
||||||
|
throw new PatchApplyException(MessageFormat.format(
|
||||||
|
JGitText.get().patchApplyException, hh));
|
||||||
|
}
|
||||||
|
newLines.remove(hh.getNewStartLine() - 1 + pos);
|
||||||
|
break;
|
||||||
|
case '+':
|
||||||
|
newLines.add(hh.getNewStartLine() - 1 + pos,
|
||||||
|
hunkLine.substring(1));
|
||||||
|
pos++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isNoNewlineAtEndOfFile(fh))
|
||||||
|
newLines.add(""); //$NON-NLS-1$
|
||||||
|
if (!rt.isMissingNewlineAtEnd())
|
||||||
|
oldLines.add(""); //$NON-NLS-1$
|
||||||
|
if (!isChanged(oldLines, newLines))
|
||||||
|
return null; // don't touch the file
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String l : newLines) {
|
||||||
|
// don't bother handling line endings - if it was windows, the \r is
|
||||||
|
// still there!
|
||||||
|
sb.append(l).append('\n');
|
||||||
|
}
|
||||||
|
sb.deleteCharAt(sb.length() - 1);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isChanged(List<String> ol, List<String> nl) {
|
||||||
|
if (ol.size() != nl.size())
|
||||||
|
return true;
|
||||||
|
for (int i = 0; i < ol.size(); i++)
|
||||||
|
if (!ol.get(i).equals(nl.get(i)))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNoNewlineAtEndOfFile(FileHeader fh) {
|
||||||
|
HunkHeader lastHunk = fh.getHunks().get(fh.getHunks().size() - 1);
|
||||||
|
RawText lhrt = new RawText(lastHunk.getBuffer());
|
||||||
|
return lhrt.getString(lhrt.size() - 1).equals(
|
||||||
|
"\\ No newline at end of file"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<logger name="scala.slick" level="INFO" />
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<logger name="service.WebHookService" level="DEBUG" />
|
||||||
|
<logger name="servlet" level="DEBUG" />
|
||||||
|
-->
|
||||||
</configuration>
|
</configuration>
|
||||||
BIN
src/main/resources/noimage.png
Normal file
BIN
src/main/resources/noimage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -1,135 +1,135 @@
|
|||||||
CREATE TABLE ACCOUNT(
|
CREATE TABLE ACCOUNT(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
MAIL_ADDRESS VARCHAR(100) NOT NULL,
|
MAIL_ADDRESS VARCHAR(100) NOT NULL,
|
||||||
PASSWORD VARCHAR(40) NOT NULL,
|
PASSWORD VARCHAR(40) NOT NULL,
|
||||||
ADMINISTRATOR BOOLEAN NOT NULL,
|
ADMINISTRATOR BOOLEAN NOT NULL,
|
||||||
URL VARCHAR(200),
|
URL VARCHAR(200),
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||||
LAST_LOGIN_DATE TIMESTAMP
|
LAST_LOGIN_DATE TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE REPOSITORY(
|
CREATE TABLE REPOSITORY(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
PRIVATE BOOLEAN NOT NULL,
|
PRIVATE BOOLEAN NOT NULL,
|
||||||
DESCRIPTION TEXT,
|
DESCRIPTION TEXT,
|
||||||
DEFAULT_BRANCH VARCHAR(100),
|
DEFAULT_BRANCH VARCHAR(100),
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||||
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
|
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE COLLABORATOR(
|
CREATE TABLE COLLABORATOR(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
COLLABORATOR_NAME VARCHAR(100) NOT NULL
|
COLLABORATOR_NAME VARCHAR(100) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ISSUE(
|
CREATE TABLE ISSUE(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
ISSUE_ID INT NOT NULL,
|
ISSUE_ID INT NOT NULL,
|
||||||
OPENED_USER_NAME VARCHAR(100) NOT NULL,
|
OPENED_USER_NAME VARCHAR(100) NOT NULL,
|
||||||
MILESTONE_ID INT,
|
MILESTONE_ID INT,
|
||||||
ASSIGNED_USER_NAME VARCHAR(100),
|
ASSIGNED_USER_NAME VARCHAR(100),
|
||||||
TITLE TEXT NOT NULL,
|
TITLE TEXT NOT NULL,
|
||||||
CONTENT TEXT,
|
CONTENT TEXT,
|
||||||
CLOSED BOOLEAN NOT NULL,
|
CLOSED BOOLEAN NOT NULL,
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL
|
UPDATED_DATE TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ISSUE_ID(
|
CREATE TABLE ISSUE_ID(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
ISSUE_ID INT NOT NULL
|
ISSUE_ID INT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ISSUE_COMMENT(
|
CREATE TABLE ISSUE_COMMENT(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
ISSUE_ID INT NOT NULL,
|
ISSUE_ID INT NOT NULL,
|
||||||
COMMENT_ID INT AUTO_INCREMENT,
|
COMMENT_ID INT AUTO_INCREMENT,
|
||||||
ACTION VARCHAR(10),
|
ACTION VARCHAR(10),
|
||||||
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
||||||
CONTENT TEXT NOT NULL,
|
CONTENT TEXT NOT NULL,
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL
|
UPDATED_DATE TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE LABEL(
|
CREATE TABLE LABEL(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
LABEL_ID INT AUTO_INCREMENT,
|
LABEL_ID INT AUTO_INCREMENT,
|
||||||
LABEL_NAME VARCHAR(100) NOT NULL,
|
LABEL_NAME VARCHAR(100) NOT NULL,
|
||||||
COLOR CHAR(6) NOT NULL
|
COLOR CHAR(6) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ISSUE_LABEL(
|
CREATE TABLE ISSUE_LABEL(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
ISSUE_ID INT NOT NULL,
|
ISSUE_ID INT NOT NULL,
|
||||||
LABEL_ID INT NOT NULL
|
LABEL_ID INT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE MILESTONE(
|
CREATE TABLE MILESTONE(
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
MILESTONE_ID INT AUTO_INCREMENT,
|
MILESTONE_ID INT AUTO_INCREMENT,
|
||||||
TITLE VARCHAR(100) NOT NULL,
|
TITLE VARCHAR(100) NOT NULL,
|
||||||
DESCRIPTION TEXT,
|
DESCRIPTION TEXT,
|
||||||
DUE_DATE TIMESTAMP,
|
DUE_DATE TIMESTAMP,
|
||||||
CLOSED_DATE TIMESTAMP
|
CLOSED_DATE TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
|
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
|
||||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
|
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
|
||||||
|
|
||||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
|
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
|
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
|
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
|
||||||
|
|
||||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
|
||||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
||||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
|
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
|
||||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||||
|
|
||||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
|
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
|
||||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
|
||||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
|
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
|
||||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||||
|
|
||||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
|
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
|
||||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
|
||||||
INSERT INTO ACCOUNT (
|
INSERT INTO ACCOUNT (
|
||||||
USER_NAME,
|
USER_NAME,
|
||||||
MAIL_ADDRESS,
|
MAIL_ADDRESS,
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
ADMINISTRATOR,
|
ADMINISTRATOR,
|
||||||
URL,
|
URL,
|
||||||
REGISTERED_DATE,
|
REGISTERED_DATE,
|
||||||
UPDATED_DATE,
|
UPDATED_DATE,
|
||||||
LAST_LOGIN_DATE
|
LAST_LOGIN_DATE
|
||||||
) VALUES (
|
) VALUES (
|
||||||
'root',
|
'root',
|
||||||
'root@localhost',
|
'root@localhost',
|
||||||
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
||||||
true,
|
true,
|
||||||
'https://github.com/takezoe/gitbucket',
|
'https://github.com/takezoe/gitbucket',
|
||||||
SYSDATE,
|
SYSDATE,
|
||||||
SYSDATE,
|
SYSDATE,
|
||||||
NULL
|
NULL
|
||||||
);
|
);
|
||||||
|
|||||||
8
src/main/resources/update/1_1.sql
Normal file
8
src/main/resources/update/1_1.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- Fix COLLABORATOR constraints
|
||||||
|
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK1 IF EXISTS;
|
||||||
|
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK0 IF EXISTS;
|
||||||
|
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_PK IF EXISTS;
|
||||||
|
|
||||||
|
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COLLABORATOR_NAME);
|
||||||
|
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
11
src/main/resources/update/1_12.sql
Normal file
11
src/main/resources/update/1_12.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
CREATE TABLE SSH_KEY (
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
SSH_KEY_ID INT AUTO_INCREMENT,
|
||||||
|
TITLE VARCHAR(100) NOT NULL,
|
||||||
|
PUBLIC_KEY TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID);
|
||||||
|
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
1
src/main/resources/update/1_13.sql
Normal file
1
src/main/resources/update/1_13.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE COMMIT_LOG;
|
||||||
24
src/main/resources/update/1_2.sql
Normal file
24
src/main/resources/update/1_2.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE ACTIVITY(
|
||||||
|
ACTIVITY_ID INT AUTO_INCREMENT,
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
ACTIVITY_USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
ACTIVITY_TYPE VARCHAR(100) NOT NULL,
|
||||||
|
MESSAGE TEXT NOT NULL,
|
||||||
|
ADDITIONAL_INFO TEXT,
|
||||||
|
ACTIVITY_DATE TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE COMMIT_LOG (
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
COMMIT_ID VARCHAR(40) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_PK PRIMARY KEY (ACTIVITY_ID);
|
||||||
|
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK1 FOREIGN KEY (ACTIVITY_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
|
|
||||||
|
ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COMMIT_ID);
|
||||||
|
ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
|
||||||
8
src/main/resources/update/1_3.sql
Normal file
8
src/main/resources/update/1_3.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE ACCOUNT ADD COLUMN IMAGE VARCHAR(100);
|
||||||
|
|
||||||
|
UPDATE ISSUE_COMMENT SET ACTION = 'comment' WHERE ACTION IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ISSUE_COMMENT ALTER COLUMN ACTION VARCHAR(20) NOT NULL;
|
||||||
|
|
||||||
|
UPDATE ISSUE_COMMENT SET ACTION = 'close_comment' WHERE ACTION = 'close';
|
||||||
|
UPDATE ISSUE_COMMENT SET ACTION = 'reopen_comment' WHERE ACTION = 'reopen';
|
||||||
24
src/main/resources/update/1_4.sql
Normal file
24
src/main/resources/update/1_4.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE GROUP_MEMBER(
|
||||||
|
GROUP_NAME VARCHAR(100) NOT NULL,
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
|
||||||
|
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
|
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||||
|
|
||||||
|
ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||||
|
SELECT
|
||||||
|
A.USER_NAME,
|
||||||
|
A.REPOSITORY_NAME,
|
||||||
|
A.ISSUE_ID,
|
||||||
|
NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
||||||
|
FROM ISSUE A
|
||||||
|
LEFT OUTER JOIN (
|
||||||
|
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||||
|
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||||
|
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||||
|
) B
|
||||||
|
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);
|
||||||
21
src/main/resources/update/1_5.sql
Normal file
21
src/main/resources/update/1_5.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100);
|
||||||
|
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100);
|
||||||
|
ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100);
|
||||||
|
ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100);
|
||||||
|
|
||||||
|
CREATE TABLE PULL_REQUEST(
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
ISSUE_ID INT NOT NULL,
|
||||||
|
BRANCH VARCHAR(100) NOT NULL,
|
||||||
|
REQUEST_USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REQUEST_BRANCH VARCHAR(100) NOT NULL,
|
||||||
|
COMMIT_ID_FROM VARCHAR(40) NOT NULL,
|
||||||
|
COMMIT_ID_TO VARCHAR(40) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||||
|
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||||
|
|
||||||
|
ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
8
src/main/resources/update/1_6.sql
Normal file
8
src/main/resources/update/1_6.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE WEB_HOOK (
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
URL VARCHAR(200) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL);
|
||||||
|
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
5
src/main/resources/update/1_7.sql
Normal file
5
src/main/resources/update/1_7.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE ACCOUNT ADD COLUMN FULL_NAME VARCHAR(100);
|
||||||
|
|
||||||
|
UPDATE ACCOUNT SET FULL_NAME = USER_NAME WHERE FULL_NAME IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ACCOUNT ALTER COLUMN FULL_NAME SET NOT NULL;
|
||||||
1
src/main/resources/update/1_8.sql
Normal file
1
src/main/resources/update/1_8.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE;
|
||||||
@@ -1,22 +1,37 @@
|
|||||||
|
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
|
||||||
import app._
|
import app._
|
||||||
|
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import javax.servlet._
|
import javax.servlet._
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
class ScalatraBootstrap extends LifeCycle {
|
class ScalatraBootstrap extends LifeCycle {
|
||||||
override def init(context: ServletContext) {
|
override def init(context: ServletContext) {
|
||||||
|
// Register TransactionFilter and BasicAuthenticationFilter at first
|
||||||
|
context.addFilter("transactionFilter", new TransactionFilter)
|
||||||
|
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||||
|
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
|
||||||
|
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||||
|
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
||||||
|
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
||||||
|
|
||||||
|
// Register controllers
|
||||||
context.mount(new IndexController, "/")
|
context.mount(new IndexController, "/")
|
||||||
context.mount(new SignInController, "/*")
|
context.mount(new SearchController, "/")
|
||||||
|
context.mount(new FileUploadController, "/upload")
|
||||||
|
context.mount(new DashboardController, "/*")
|
||||||
context.mount(new UserManagementController, "/*")
|
context.mount(new UserManagementController, "/*")
|
||||||
context.mount(new SystemSettingsController, "/*")
|
context.mount(new SystemSettingsController, "/*")
|
||||||
context.mount(new CreateRepositoryController, "/*")
|
|
||||||
context.mount(new AccountController, "/*")
|
context.mount(new AccountController, "/*")
|
||||||
context.mount(new RepositoryViewerController, "/*")
|
context.mount(new RepositoryViewerController, "/*")
|
||||||
context.mount(new WikiController, "/*")
|
context.mount(new WikiController, "/*")
|
||||||
context.mount(new LabelsController, "/*")
|
context.mount(new LabelsController, "/*")
|
||||||
context.mount(new MilestonesController, "/*")
|
context.mount(new MilestonesController, "/*")
|
||||||
context.mount(new IssuesController, "/*")
|
context.mount(new IssuesController, "/*")
|
||||||
context.mount(new SettingsController, "/*")
|
context.mount(new PullRequestsController, "/*")
|
||||||
|
context.mount(new RepositorySettingsController, "/*")
|
||||||
|
|
||||||
|
// Create GITBUCKET_HOME directory if it does not exist
|
||||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
||||||
if(!dir.exists){
|
if(!dir.exists){
|
||||||
dir.mkdirs()
|
dir.mkdirs()
|
||||||
|
|||||||
@@ -1,75 +1,434 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.OneselfAuthenticator
|
import util._
|
||||||
import util.StringUtil._
|
import util.StringUtil._
|
||||||
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Implicits._
|
||||||
|
import ssh.SshUtil
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import model.GroupMember
|
||||||
|
|
||||||
class AccountController extends AccountControllerBase
|
class AccountController extends AccountControllerBase
|
||||||
with SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator
|
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||||
|
|
||||||
trait AccountControllerBase extends ControllerBase {
|
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||||
self: SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator =>
|
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
|
||||||
|
|
||||||
case class AccountNewForm(userName: String, password: String,mailAddress: String, url: Option[String])
|
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||||
|
url: Option[String], fileId: Option[String])
|
||||||
|
|
||||||
case class AccountEditForm(password: Option[String], mailAddress: String, url: Option[String])
|
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
|
||||||
|
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||||
|
|
||||||
|
case class SshKeyForm(title: String, publicKey: String)
|
||||||
|
|
||||||
val newForm = mapping(
|
val newForm = mapping(
|
||||||
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, unique))),
|
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100)))),
|
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
|
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" , optional(text())))
|
||||||
)(AccountNewForm.apply)
|
)(AccountNewForm.apply)
|
||||||
|
|
||||||
val editForm = mapping(
|
val editForm = mapping(
|
||||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100)))),
|
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||||
|
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||||
|
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||||
)(AccountEditForm.apply)
|
)(AccountEditForm.apply)
|
||||||
|
|
||||||
|
val sshKeyForm = mapping(
|
||||||
|
"title" -> trim(label("Title", text(required, maxlength(100)))),
|
||||||
|
"publicKey" -> trim(label("Key" , text(required, validPublicKey)))
|
||||||
|
)(SshKeyForm.apply)
|
||||||
|
|
||||||
|
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
|
||||||
|
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
|
||||||
|
|
||||||
|
val newGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members)))
|
||||||
|
)(NewGroupForm.apply)
|
||||||
|
|
||||||
|
val editGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean()))
|
||||||
|
)(EditGroupForm.apply)
|
||||||
|
|
||||||
|
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
||||||
|
case class ForkRepositoryForm(owner: String, name: String)
|
||||||
|
|
||||||
|
val newRepositoryForm = mapping(
|
||||||
|
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
|
||||||
|
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
|
||||||
|
"description" -> trim(label("Description" , optional(text()))),
|
||||||
|
"isPrivate" -> trim(label("Repository Type", boolean())),
|
||||||
|
"createReadme" -> trim(label("Create README" , boolean()))
|
||||||
|
)(RepositoryCreationForm.apply)
|
||||||
|
|
||||||
|
val forkRepositoryForm = mapping(
|
||||||
|
"owner" -> trim(label("Repository owner", text(required))),
|
||||||
|
"name" -> trim(label("Repository name", text(required)))
|
||||||
|
)(ForkRepositoryForm.apply)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays user information.
|
* Displays user information.
|
||||||
*/
|
*/
|
||||||
get("/:userName") {
|
get("/:userName") {
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map {
|
getAccountByUserName(userName).map { account =>
|
||||||
account.html.info(_, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
|
params.getOrElse("tab", "repositories") match {
|
||||||
|
// Public Activity
|
||||||
|
case "activity" =>
|
||||||
|
_root_.account.html.activity(account,
|
||||||
|
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||||
|
getActivitiesByUser(userName, true))
|
||||||
|
|
||||||
|
// Members
|
||||||
|
case "members" if(account.isGroupAccount) => {
|
||||||
|
val members = getGroupMembers(account.userName)
|
||||||
|
_root_.account.html.members(account, members.map(_.userName),
|
||||||
|
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
case _ => {
|
||||||
|
val members = getGroupMembers(account.userName)
|
||||||
|
_root_.account.html.repositories(account,
|
||||||
|
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||||
|
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
|
||||||
|
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
||||||
|
}
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get("/:userName.atom") {
|
||||||
|
val userName = params("userName")
|
||||||
|
contentType = "application/atom+xml; type=feed"
|
||||||
|
helper.xml.feed(getActivitiesByUser(userName, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/:userName/_avatar"){
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
||||||
|
contentType = FileUtil.getMimeType(image)
|
||||||
|
new java.io.File(getUserUploadDir(userName), image)
|
||||||
|
} getOrElse {
|
||||||
|
contentType = "image/png"
|
||||||
|
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get("/:userName/_edit")(oneselfOnly {
|
get("/:userName/_edit")(oneselfOnly {
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map(x => account.html.edit(Some(x))) getOrElse NotFound
|
getAccountByUserName(userName).map { x =>
|
||||||
|
account.html.edit(x, flash.get("info"))
|
||||||
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:userName/_edit", editForm)(oneselfOnly { form =>
|
post("/:userName/_edit", editForm)(oneselfOnly { form =>
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map { account =>
|
getAccountByUserName(userName).map { account =>
|
||||||
updateAccount(account.copy(
|
updateAccount(account.copy(
|
||||||
password = form.password.map(encrypt).getOrElse(account.password),
|
password = form.password.map(sha1).getOrElse(account.password),
|
||||||
|
fullName = form.fullName,
|
||||||
mailAddress = form.mailAddress,
|
mailAddress = form.mailAddress,
|
||||||
url = form.url))
|
url = form.url))
|
||||||
redirect("/%s".format(userName))
|
|
||||||
|
updateImage(userName, form.fileId, form.clearImage)
|
||||||
|
flash += "info" -> "Account information has been updated."
|
||||||
|
redirect(s"/${userName}/_edit")
|
||||||
|
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/:userName/_delete")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
|
||||||
|
getAccountByUserName(userName, true).foreach { account =>
|
||||||
|
// Remove repositories
|
||||||
|
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||||
|
deleteRepository(userName, repositoryName)
|
||||||
|
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||||
|
}
|
||||||
|
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||||
|
removeUserRelatedData(userName)
|
||||||
|
|
||||||
|
updateAccount(account.copy(isRemoved = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
session.invalidate
|
||||||
|
redirect("/")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:userName/_ssh")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).map { x =>
|
||||||
|
account.html.ssh(x, getPublicKeys(x.userName))
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
|
||||||
|
val userName = params("userName")
|
||||||
|
addPublicKey(userName, form.title, form.publicKey)
|
||||||
|
redirect(s"/${userName}/_ssh")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:userName/_ssh/delete/:id")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
val sshKeyId = params("id").toInt
|
||||||
|
deletePublicKey(userName, sshKeyId)
|
||||||
|
redirect(s"/${userName}/_ssh")
|
||||||
|
})
|
||||||
|
|
||||||
get("/register"){
|
get("/register"){
|
||||||
if(loadSystemSettings().allowAccountRegistration){
|
if(context.settings.allowAccountRegistration){
|
||||||
account.html.edit(None)
|
if(context.loginAccount.isDefined){
|
||||||
|
redirect("/")
|
||||||
|
} else {
|
||||||
|
account.html.register()
|
||||||
|
}
|
||||||
} else NotFound
|
} else NotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/register", newForm){ newForm =>
|
post("/register", newForm){ form =>
|
||||||
if(loadSystemSettings().allowAccountRegistration){
|
if(context.settings.allowAccountRegistration){
|
||||||
createAccount(newForm.userName, encrypt(newForm.password), newForm.mailAddress, false, newForm.url)
|
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
|
||||||
|
updateImage(form.userName, form.fileId, false)
|
||||||
redirect("/signin")
|
redirect("/signin")
|
||||||
} else NotFound
|
} else NotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
private def unique: Constraint = new Constraint(){
|
get("/groups/new")(usersOnly {
|
||||||
def validate(name: String, value: String): Option[String] =
|
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
|
||||||
getAccountByUserName(value).map { _ => "User already exists." }
|
})
|
||||||
|
|
||||||
|
post("/groups/new", newGroupForm)(usersOnly { form =>
|
||||||
|
createGroup(form.groupName, form.url)
|
||||||
|
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList)
|
||||||
|
updateImage(form.groupName, form.fileId, false)
|
||||||
|
redirect(s"/${form.groupName}")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:groupName/_editgroup")(managersOnly {
|
||||||
|
defining(params("groupName")){ groupName =>
|
||||||
|
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:groupName/_deletegroup")(managersOnly {
|
||||||
|
defining(params("groupName")){ groupName =>
|
||||||
|
// Remove from GROUP_MEMBER
|
||||||
|
updateGroupMembers(groupName, Nil)
|
||||||
|
// Remove repositories
|
||||||
|
getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
|
||||||
|
deleteRepository(groupName, repositoryName)
|
||||||
|
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redirect("/")
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:groupName/_editgroup", editGroupForm)(managersOnly { form =>
|
||||||
|
defining(params("groupName"), form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList){ case (groupName, members) =>
|
||||||
|
getAccountByUserName(groupName, true).map { account =>
|
||||||
|
updateGroup(groupName, form.url, false)
|
||||||
|
|
||||||
|
// Update GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, members)
|
||||||
|
// Update COLLABORATOR for group repositories
|
||||||
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
|
removeCollaborators(form.groupName, repositoryName)
|
||||||
|
members.foreach { case (userName, isManager) =>
|
||||||
|
addCollaborator(form.groupName, repositoryName, userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||||
|
redirect(s"/${form.groupName}")
|
||||||
|
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the new repository form.
|
||||||
|
*/
|
||||||
|
get("/new")(usersOnly {
|
||||||
|
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new repository.
|
||||||
|
*/
|
||||||
|
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||||
|
LockUtil.lock(s"${form.owner}/${form.name}"){
|
||||||
|
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
|
||||||
|
val ownerAccount = getAccountByUserName(form.owner).get
|
||||||
|
val loginAccount = context.loginAccount.get
|
||||||
|
val loginUserName = loginAccount.userName
|
||||||
|
|
||||||
|
// Insert to the database at first
|
||||||
|
createRepository(form.name, form.owner, form.description, form.isPrivate)
|
||||||
|
|
||||||
|
// Add collaborators for group repository
|
||||||
|
if(ownerAccount.isGroupAccount){
|
||||||
|
getGroupMembers(form.owner).foreach { member =>
|
||||||
|
addCollaborator(form.owner, form.name, member.userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert default labels
|
||||||
|
insertDefaultLabels(form.owner, form.name)
|
||||||
|
|
||||||
|
// Create the actual repository
|
||||||
|
val gitdir = getRepositoryDir(form.owner, form.name)
|
||||||
|
JGitUtil.initRepository(gitdir)
|
||||||
|
|
||||||
|
if(form.createReadme){
|
||||||
|
using(Git.open(gitdir)){ git =>
|
||||||
|
val builder = DirCache.newInCore.builder()
|
||||||
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
val content = if(form.description.nonEmpty){
|
||||||
|
form.name + "\n" +
|
||||||
|
"===============\n" +
|
||||||
|
"\n" +
|
||||||
|
form.description.get
|
||||||
|
} else {
|
||||||
|
form.name + "\n" +
|
||||||
|
"===============\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||||
|
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||||
|
builder.finish()
|
||||||
|
|
||||||
|
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||||
|
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Wiki repository
|
||||||
|
createWikiRepository(loginAccount, form.owner, form.name)
|
||||||
|
|
||||||
|
// Record activity
|
||||||
|
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect to the repository
|
||||||
|
redirect(s"/${form.owner}/${form.name}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||||
|
val loginAccount = context.loginAccount.get
|
||||||
|
val loginUserName = loginAccount.userName
|
||||||
|
|
||||||
|
LockUtil.lock(s"${loginUserName}/${repository.name}"){
|
||||||
|
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
|
||||||
|
// redirect to the repository if repository already exists
|
||||||
|
redirect(s"/${loginUserName}/${repository.name}")
|
||||||
|
} else {
|
||||||
|
// Insert to the database at first
|
||||||
|
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||||
|
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||||
|
|
||||||
|
createRepository(
|
||||||
|
repositoryName = repository.name,
|
||||||
|
userName = loginUserName,
|
||||||
|
description = repository.repository.description,
|
||||||
|
isPrivate = repository.repository.isPrivate,
|
||||||
|
originRepositoryName = Some(originRepositoryName),
|
||||||
|
originUserName = Some(originUserName),
|
||||||
|
parentRepositoryName = Some(repository.name),
|
||||||
|
parentUserName = Some(repository.owner)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Insert default labels
|
||||||
|
insertDefaultLabels(loginUserName, repository.name)
|
||||||
|
|
||||||
|
// clone repository actually
|
||||||
|
JGitUtil.cloneRepository(
|
||||||
|
getRepositoryDir(repository.owner, repository.name),
|
||||||
|
getRepositoryDir(loginUserName, repository.name))
|
||||||
|
|
||||||
|
// Create Wiki repository
|
||||||
|
JGitUtil.cloneRepository(
|
||||||
|
getWikiRepositoryDir(repository.owner, repository.name),
|
||||||
|
getWikiRepositoryDir(loginUserName, repository.name))
|
||||||
|
|
||||||
|
// Record activity
|
||||||
|
recordForkActivity(repository.owner, repository.name, loginUserName)
|
||||||
|
// redirect to the repository
|
||||||
|
redirect(s"/${loginUserName}/${repository.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
||||||
|
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||||
|
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
||||||
|
createLabel(userName, repositoryName, "enhancement", "84b6eb")
|
||||||
|
createLabel(userName, repositoryName, "invalid", "e6e6e6")
|
||||||
|
createLabel(userName, repositoryName, "question", "cc317c")
|
||||||
|
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def existsAccount: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||||
|
}
|
||||||
|
|
||||||
|
private def uniqueRepository: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||||
|
params.get("owner").flatMap { userName =>
|
||||||
|
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def members: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
if(value.split(",").exists {
|
||||||
|
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
||||||
|
}) None else Some("Must select one manager at least.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def validPublicKey: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match {
|
||||||
|
case Some(_) => None
|
||||||
|
case None => Some("Key is invalid.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,201 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import model.Account
|
import _root_.util.Directory._
|
||||||
import util.Validations
|
import _root_.util.Implicits._
|
||||||
|
import _root_.util.ControlUtil._
|
||||||
|
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import org.scalatra.json._
|
import org.scalatra.json._
|
||||||
import org.json4s._
|
import org.json4s._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import model._
|
||||||
|
import service.{SystemSettingsService, AccountService}
|
||||||
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
|
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||||
|
import org.scalatra.i18n._
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides generic features for ScalatraServlet implementations.
|
* Provides generic features for controller implementations.
|
||||||
*/
|
*/
|
||||||
abstract class ControllerBase extends ScalatraFilter
|
abstract class ControllerBase extends ScalatraFilter
|
||||||
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
|
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
|
||||||
|
with SystemSettingsService {
|
||||||
|
|
||||||
implicit val jsonFormats = DefaultFormats
|
implicit val jsonFormats = DefaultFormats
|
||||||
|
|
||||||
|
// TODO Scala 2.11
|
||||||
|
// // Don't set content type via Accept header.
|
||||||
|
// override def format(implicit request: HttpServletRequest) = ""
|
||||||
|
|
||||||
|
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
|
||||||
|
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||||
|
val httpResponse = response.asInstanceOf[HttpServletResponse]
|
||||||
|
val context = request.getServletContext.getContextPath
|
||||||
|
val path = httpRequest.getRequestURI.substring(context.length)
|
||||||
|
|
||||||
|
if(path.startsWith("/console/")){
|
||||||
|
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||||
|
val baseUrl = this.baseUrl(httpRequest)
|
||||||
|
if(account == null){
|
||||||
|
// Redirect to login form
|
||||||
|
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
|
||||||
|
} else if(account.isAdmin){
|
||||||
|
// H2 Console (administrators only)
|
||||||
|
chain.doFilter(request, response)
|
||||||
|
} else {
|
||||||
|
// Redirect to dashboard
|
||||||
|
httpResponse.sendRedirect(baseUrl + "/")
|
||||||
|
}
|
||||||
|
} else if(path.startsWith("/git/")){
|
||||||
|
// Git repository
|
||||||
|
chain.doFilter(request, response)
|
||||||
|
} else {
|
||||||
|
// Scalatra actions
|
||||||
|
super.doFilter(request, response, chain)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
contextCache.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contextCache = new java.lang.ThreadLocal[Context]()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the context object for the request.
|
* Returns the context object for the request.
|
||||||
*/
|
*/
|
||||||
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL)
|
implicit def context: Context = {
|
||||||
|
contextCache.get match {
|
||||||
private def currentURL: String = {
|
case null => {
|
||||||
val queryString = request.getQueryString
|
val context = Context(loadSystemSettings(), LoginAccount, request)
|
||||||
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
|
contextCache.set(context)
|
||||||
}
|
context
|
||||||
|
}
|
||||||
private def LoginAccount: Option[Account] = {
|
case context => context
|
||||||
session.get("LOGIN_ACCOUNT") match {
|
|
||||||
case Some(x: Account) => Some(x)
|
|
||||||
case _ => None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def ajaxGet(path : String)(action : => Any) : Route = {
|
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
|
||||||
|
|
||||||
|
def ajaxGet(path : String)(action : => Any) : Route =
|
||||||
super.get(path){
|
super.get(path){
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action
|
action
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
|
override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||||
super.ajaxGet(path, form){ form =>
|
super.ajaxGet(path, form){ form =>
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action(form)
|
action(form)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def ajaxPost(path : String)(action : => Any) : Route = {
|
def ajaxPost(path : String)(action : => Any) : Route =
|
||||||
super.post(path){
|
super.post(path){
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action
|
action
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
|
override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||||
super.ajaxPost(path, form){ form =>
|
super.ajaxPost(path, form){ form =>
|
||||||
request.setAttribute("AJAX", "true")
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
action(form)
|
action(form)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected def NotFound() = {
|
protected def NotFound() =
|
||||||
if(request.getAttribute("AJAX") == null){
|
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||||
org.scalatra.NotFound(html.error("Not Found"))
|
|
||||||
} else {
|
|
||||||
org.scalatra.NotFound()
|
org.scalatra.NotFound()
|
||||||
|
} else {
|
||||||
|
org.scalatra.NotFound(html.error("Not Found"))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected def Unauthorized()(implicit context: app.Context) = {
|
protected def Unauthorized()(implicit context: app.Context) =
|
||||||
if(request.getAttribute("AJAX") == null){
|
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||||
|
org.scalatra.Unauthorized()
|
||||||
|
} else {
|
||||||
if(context.loginAccount.isDefined){
|
if(context.loginAccount.isDefined){
|
||||||
org.scalatra.Unauthorized(redirect("/"))
|
org.scalatra.Unauthorized(redirect("/"))
|
||||||
} else {
|
} else {
|
||||||
org.scalatra.Unauthorized(redirect("/signin?" + currentURL))
|
if(request.getMethod.toUpperCase == "POST"){
|
||||||
|
org.scalatra.Unauthorized(redirect("/signin"))
|
||||||
|
} else {
|
||||||
|
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
|
||||||
|
defining(request.getQueryString){ queryString =>
|
||||||
|
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
org.scalatra.Unauthorized()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected def baseUrl = {
|
// TODO Scala 2.11
|
||||||
val url = request.getRequestURL.toString
|
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
|
||||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
includeContextPath: Boolean = true, includeServletPath: Boolean = true,
|
||||||
}
|
absolutize: Boolean = true, withSessionId: Boolean = true)
|
||||||
|
(implicit request: HttpServletRequest, response: HttpServletResponse): String =
|
||||||
|
if (path.startsWith("http")) path
|
||||||
|
else baseUrl + super.url(path, params, false, false, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Context(path: String, loginAccount: Option[Account], currentUrl: String)
|
/**
|
||||||
|
* Context object for the current request.
|
||||||
|
*/
|
||||||
|
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
|
||||||
|
|
||||||
|
val path = settings.baseUrl.getOrElse(request.getContextPath)
|
||||||
|
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
||||||
|
val baseUrl = settings.baseUrl(request)
|
||||||
|
val host = new java.net.URL(baseUrl).getHost
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get object from cache.
|
||||||
|
*
|
||||||
|
* If object has not been cached with the specified key then retrieves by given action.
|
||||||
|
* Cached object are available during a request.
|
||||||
|
*/
|
||||||
|
def cache[A](key: String)(action: => A): A =
|
||||||
|
defining(Keys.Request.Cache(key)){ cacheKey =>
|
||||||
|
Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
|
||||||
|
val newObject = action
|
||||||
|
request.setAttribute(cacheKey, newObject)
|
||||||
|
newObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base trait for controllers which manages account information.
|
||||||
|
*/
|
||||||
|
trait AccountManagementControllerBase extends ControllerBase {
|
||||||
|
self: AccountService =>
|
||||||
|
|
||||||
|
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
|
||||||
|
if(clearImage){
|
||||||
|
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
||||||
|
new java.io.File(getUserUploadDir(userName), image).delete()
|
||||||
|
updateAvatarImage(userName, None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileId.map { fileId =>
|
||||||
|
val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
|
||||||
|
FileUtils.moveFile(
|
||||||
|
new java.io.File(getTemporaryDir(session.getId), fileId),
|
||||||
|
new java.io.File(getUserUploadDir(userName), filename)
|
||||||
|
)
|
||||||
|
updateAvatarImage(userName, Some(filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def uniqueUserName: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
getAccountByUserName(value, true).map { _ => "User already exists." }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||||
|
getAccountByMailAddress(value, true)
|
||||||
|
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
||||||
|
.map { _ => "Mail address is already registered." }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import util.Directory._
|
|
||||||
import util.UsersAuthenticator
|
|
||||||
import service._
|
|
||||||
import java.io.File
|
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import org.eclipse.jgit.lib._
|
|
||||||
import org.apache.commons.io._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
class CreateRepositoryController extends CreateRepositoryControllerBase
|
|
||||||
with RepositoryService with AccountService with WikiService with LabelsService with UsersAuthenticator
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new repository.
|
|
||||||
*/
|
|
||||||
trait CreateRepositoryControllerBase extends ControllerBase {
|
|
||||||
self: RepositoryService with WikiService with LabelsService with UsersAuthenticator =>
|
|
||||||
|
|
||||||
case class RepositoryCreationForm(name: String, description: Option[String])
|
|
||||||
|
|
||||||
val form = mapping(
|
|
||||||
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
|
|
||||||
"description" -> trim(label("Description" , optional(text())))
|
|
||||||
)(RepositoryCreationForm.apply)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the new repository form.
|
|
||||||
*/
|
|
||||||
get("/new")(usersOnly {
|
|
||||||
html.newrepo()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new repository.
|
|
||||||
*/
|
|
||||||
post("/new", form)(usersOnly { form =>
|
|
||||||
val loginUserName = context.loginAccount.get.userName
|
|
||||||
|
|
||||||
// Insert to the database at first
|
|
||||||
createRepository(form.name, loginUserName, form.description)
|
|
||||||
|
|
||||||
// Insert default labels
|
|
||||||
createLabel(loginUserName, form.name, "bug", "fc2929")
|
|
||||||
createLabel(loginUserName, form.name, "duplicate", "cccccc")
|
|
||||||
createLabel(loginUserName, form.name, "enhancement", "84b6eb")
|
|
||||||
createLabel(loginUserName, form.name, "invalid", "e6e6e6")
|
|
||||||
createLabel(loginUserName, form.name, "question", "cc317c")
|
|
||||||
createLabel(loginUserName, form.name, "wontfix", "ffffff")
|
|
||||||
|
|
||||||
// Create the actual repository
|
|
||||||
val gitdir = getRepositoryDir(loginUserName, form.name)
|
|
||||||
val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build
|
|
||||||
|
|
||||||
repository.create
|
|
||||||
|
|
||||||
val config = repository.getConfig
|
|
||||||
config.setBoolean("http", null, "receivepack", true)
|
|
||||||
config.save
|
|
||||||
|
|
||||||
val tmpdir = getInitRepositoryDir(loginUserName, form.name)
|
|
||||||
try {
|
|
||||||
// Clone the repository
|
|
||||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
|
||||||
|
|
||||||
// Create README.md
|
|
||||||
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
|
|
||||||
if(form.description.nonEmpty){
|
|
||||||
form.name + "\n===============\n\n" + form.description.get
|
|
||||||
} else {
|
|
||||||
form.name + "\n===============\n"
|
|
||||||
}, "UTF-8")
|
|
||||||
|
|
||||||
val git = Git.open(tmpdir)
|
|
||||||
git.add.addFilepattern("README.md").call
|
|
||||||
git.commit.setMessage("Initial commit").call
|
|
||||||
git.push.call
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Wiki repository
|
|
||||||
createWikiRepository(context.loginAccount.get, form.name)
|
|
||||||
|
|
||||||
// redirect to the repository
|
|
||||||
redirect("/%s/%s".format(loginUserName, form.name))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Duplicate check for the repository name.
|
|
||||||
*/
|
|
||||||
private def unique: Constraint = new Constraint(){
|
|
||||||
def validate(name: String, value: String): Option[String] =
|
|
||||||
getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
110
src/main/scala/app/DashboardController.scala
Normal file
110
src/main/scala/app/DashboardController.scala
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import service._
|
||||||
|
import util.{UsersAuthenticator, Keys}
|
||||||
|
import util.Implicits._
|
||||||
|
|
||||||
|
class DashboardController extends DashboardControllerBase
|
||||||
|
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||||
|
with UsersAuthenticator
|
||||||
|
|
||||||
|
trait DashboardControllerBase extends ControllerBase {
|
||||||
|
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
|
||||||
|
|
||||||
|
get("/dashboard/issues/repos")(usersOnly {
|
||||||
|
searchIssues("all")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/issues/assigned")(usersOnly {
|
||||||
|
searchIssues("assigned")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/issues/created_by")(usersOnly {
|
||||||
|
searchIssues("created_by")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls")(usersOnly {
|
||||||
|
searchPullRequests("created_by", None)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls/owned")(usersOnly {
|
||||||
|
searchPullRequests("created_by", None)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls/public")(usersOnly {
|
||||||
|
searchPullRequests("not_created_by", None)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
|
||||||
|
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
|
||||||
|
})
|
||||||
|
|
||||||
|
private def searchIssues(filter: String) = {
|
||||||
|
import IssuesService._
|
||||||
|
|
||||||
|
// condition
|
||||||
|
val condition = session.putAndGet(Keys.Session.DashboardIssues,
|
||||||
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
|
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
|
||||||
|
)
|
||||||
|
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
||||||
|
val filterUser = Map(filter -> userName)
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|
||||||
|
dashboard.html.issues(
|
||||||
|
issues.html.listparts(
|
||||||
|
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
||||||
|
page,
|
||||||
|
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
|
||||||
|
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
|
||||||
|
condition),
|
||||||
|
countIssue(condition, Map.empty, false, userRepos: _*),
|
||||||
|
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*),
|
||||||
|
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
|
||||||
|
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
|
||||||
|
condition,
|
||||||
|
filter)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private def searchPullRequests(filter: String, repository: Option[String]) = {
|
||||||
|
import IssuesService._
|
||||||
|
import PullRequestService._
|
||||||
|
|
||||||
|
// condition
|
||||||
|
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
|
||||||
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
|
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
|
||||||
|
}.copy(repo = repository))
|
||||||
|
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
val allRepos = getAllRepositories()
|
||||||
|
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
||||||
|
val filterUser = Map(filter -> userName)
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|
||||||
|
val counts = countIssueGroupByRepository(
|
||||||
|
IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*)
|
||||||
|
|
||||||
|
dashboard.html.pulls(
|
||||||
|
pulls.html.listparts(
|
||||||
|
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
||||||
|
page,
|
||||||
|
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
|
||||||
|
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
|
||||||
|
condition,
|
||||||
|
None,
|
||||||
|
false),
|
||||||
|
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
|
||||||
|
userRepos.map { case (userName, repoName) =>
|
||||||
|
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
|
||||||
|
}.sortBy(_._3).reverse,
|
||||||
|
condition,
|
||||||
|
filter)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
44
src/main/scala/app/FileUploadController.scala
Normal file
44
src/main/scala/app/FileUploadController.scala
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import util.{Keys, FileUtil}
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Directory._
|
||||||
|
import org.scalatra._
|
||||||
|
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Ajax based file upload functionality.
|
||||||
|
*
|
||||||
|
* This servlet saves uploaded file.
|
||||||
|
*/
|
||||||
|
class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
||||||
|
|
||||||
|
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||||
|
|
||||||
|
post("/image"){
|
||||||
|
execute { (file, fileId) =>
|
||||||
|
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
|
||||||
|
session += Keys.Session.Upload(fileId) -> file.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/image/:owner/:repository"){
|
||||||
|
execute { (file, fileId) =>
|
||||||
|
FileUtils.writeByteArrayToFile(new java.io.File(
|
||||||
|
getAttachedDir(params("owner"), params("repository")),
|
||||||
|
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
|
||||||
|
case Some(file) if(FileUtil.isImage(file.name)) =>
|
||||||
|
defining(FileUtil.generateFileId){ fileId =>
|
||||||
|
f(file, fileId)
|
||||||
|
|
||||||
|
Ok(fileId)
|
||||||
|
}
|
||||||
|
case _ => BadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,14 +1,106 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import util._
|
||||||
|
import util.Implicits._
|
||||||
class IndexController extends IndexControllerBase with RepositoryService with AccountService with SystemSettingsService
|
import service._
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
trait IndexControllerBase extends ControllerBase { self: RepositoryService with SystemSettingsService =>
|
|
||||||
|
class IndexController extends IndexControllerBase
|
||||||
get("/"){
|
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
|
||||||
html.index(getAccessibleRepositories(context.loginAccount, baseUrl), loadSystemSettings(),
|
|
||||||
context.loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil))
|
trait IndexControllerBase extends ControllerBase {
|
||||||
}
|
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
|
||||||
|
|
||||||
}
|
case class SignInForm(userName: String, password: String)
|
||||||
|
|
||||||
|
val form = mapping(
|
||||||
|
"userName" -> trim(label("Username", text(required))),
|
||||||
|
"password" -> trim(label("Password", text(required)))
|
||||||
|
)(SignInForm.apply)
|
||||||
|
|
||||||
|
get("/"){
|
||||||
|
val loginAccount = context.loginAccount
|
||||||
|
if(loginAccount.isEmpty) {
|
||||||
|
html.index(getRecentActivities(),
|
||||||
|
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
|
||||||
|
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val loginUserName = loginAccount.get.userName
|
||||||
|
val loginUserGroups = getGroupsByUserName(loginUserName)
|
||||||
|
var visibleOwnerSet : Set[String] = Set(loginUserName)
|
||||||
|
|
||||||
|
visibleOwnerSet ++= loginUserGroups
|
||||||
|
|
||||||
|
html.index(getRecentActivitiesByOwners(visibleOwnerSet),
|
||||||
|
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
|
||||||
|
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/signin"){
|
||||||
|
val redirect = params.get("redirect")
|
||||||
|
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||||
|
flash += Keys.Flash.Redirect -> redirect.get
|
||||||
|
}
|
||||||
|
html.signin()
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/signin", form){ form =>
|
||||||
|
authenticate(context.settings, form.userName, form.password) match {
|
||||||
|
case Some(account) => signin(account)
|
||||||
|
case None => redirect("/signin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/signout"){
|
||||||
|
session.invalidate
|
||||||
|
redirect("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/activities.atom"){
|
||||||
|
contentType = "application/atom+xml; type=feed"
|
||||||
|
helper.xml.feed(getRecentActivities())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set account information into HttpSession and redirect.
|
||||||
|
*/
|
||||||
|
private def signin(account: model.Account) = {
|
||||||
|
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||||
|
updateLastLoginDate(account.userName)
|
||||||
|
|
||||||
|
if(LDAPUtil.isDummyMailAddress(account)) {
|
||||||
|
redirect("/" + account.userName + "/_edit")
|
||||||
|
}
|
||||||
|
|
||||||
|
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
|
||||||
|
if(redirectUrl.stripSuffix("/") == request.getContextPath){
|
||||||
|
redirect("/")
|
||||||
|
} else {
|
||||||
|
redirect(redirectUrl)
|
||||||
|
}
|
||||||
|
}.getOrElse {
|
||||||
|
redirect("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON API for collaborator completion.
|
||||||
|
*/
|
||||||
|
get("/_user/proposals")(usersOnly {
|
||||||
|
contentType = formats("json")
|
||||||
|
org.json4s.jackson.Serialization.write(
|
||||||
|
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON APU for checking user existence.
|
||||||
|
*/
|
||||||
|
post("/_user/existence")(usersOnly {
|
||||||
|
getAccountByUserName(params("userName")).isDefined
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,252 +1,403 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator}
|
import util._
|
||||||
import org.scalatra.Ok
|
import util.Implicits._
|
||||||
|
import util.ControlUtil._
|
||||||
class IssuesController extends IssuesControllerBase
|
import org.scalatra.Ok
|
||||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService
|
import model.Issue
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
|
||||||
|
class IssuesController extends IssuesControllerBase
|
||||||
trait IssuesControllerBase extends ControllerBase {
|
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||||
self: IssuesService with RepositoryService with LabelsService with MilestonesService
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
|
||||||
|
trait IssuesControllerBase extends ControllerBase {
|
||||||
case class IssueCreateForm(title: String, content: Option[String],
|
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||||
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
case class IssueEditForm(title: String, content: Option[String])
|
case class IssueCreateForm(title: String, content: Option[String],
|
||||||
|
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
|
||||||
case class CommentForm(issueId: Int, content: String)
|
case class IssueEditForm(title: String, content: Option[String])
|
||||||
|
case class CommentForm(issueId: Int, content: String)
|
||||||
val issueCreateForm = mapping(
|
case class IssueStateForm(issueId: Int, content: Option[String])
|
||||||
"title" -> trim(label("Title", text(required))),
|
|
||||||
"content" -> trim(optional(text())),
|
val issueCreateForm = mapping(
|
||||||
"assignedUserName" -> trim(optional(text())),
|
"title" -> trim(label("Title", text(required))),
|
||||||
"milestoneId" -> trim(optional(number())),
|
"content" -> trim(optional(text())),
|
||||||
"labelNames" -> trim(optional(text()))
|
"assignedUserName" -> trim(optional(text())),
|
||||||
)(IssueCreateForm.apply)
|
"milestoneId" -> trim(optional(number())),
|
||||||
|
"labelNames" -> trim(optional(text()))
|
||||||
val issueEditForm = mapping(
|
)(IssueCreateForm.apply)
|
||||||
"title" -> trim(label("Title", text(required))),
|
|
||||||
"content" -> trim(optional(text()))
|
val issueEditForm = mapping(
|
||||||
)(IssueEditForm.apply)
|
"title" -> trim(label("Title", text(required))),
|
||||||
|
"content" -> trim(optional(text()))
|
||||||
val commentForm = mapping(
|
)(IssueEditForm.apply)
|
||||||
"issueId" -> label("Issue Id", number()),
|
|
||||||
"content" -> trim(label("Comment", text(required)))
|
val commentForm = mapping(
|
||||||
)(CommentForm.apply)
|
"issueId" -> label("Issue Id", number()),
|
||||||
|
"content" -> trim(label("Comment", text(required)))
|
||||||
get("/:owner/:repository/issues")(referrersOnly {
|
)(CommentForm.apply)
|
||||||
searchIssues("all", _)
|
|
||||||
})
|
val issueStateForm = mapping(
|
||||||
|
"issueId" -> label("Issue Id", number()),
|
||||||
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
|
"content" -> trim(optional(text()))
|
||||||
searchIssues("assigned", _)
|
)(IssueStateForm.apply)
|
||||||
})
|
|
||||||
|
get("/:owner/:repository/issues")(referrersOnly {
|
||||||
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
|
searchIssues("all", _)
|
||||||
searchIssues("created_by", _)
|
})
|
||||||
})
|
|
||||||
|
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
|
||||||
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
searchIssues("assigned", _)
|
||||||
val owner = repository.owner
|
})
|
||||||
val name = repository.name
|
|
||||||
val issueId = params("id")
|
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
|
||||||
|
searchIssues("created_by", _)
|
||||||
getIssue(owner, name, issueId) map {
|
})
|
||||||
issues.html.issue(
|
|
||||||
_,
|
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
||||||
getComments(owner, name, issueId.toInt),
|
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
|
||||||
getIssueLabels(owner, name, issueId.toInt),
|
getIssue(owner, name, issueId) map {
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
issues.html.issue(
|
||||||
getMilestones(owner, name),
|
_,
|
||||||
getLabels(owner, name),
|
getComments(owner, name, issueId.toInt),
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
getIssueLabels(owner, name, issueId.toInt),
|
||||||
repository)
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
} getOrElse NotFound
|
getMilestonesWithIssueCount(owner, name),
|
||||||
})
|
getLabels(owner, name),
|
||||||
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
repository)
|
||||||
val owner = repository.owner
|
} getOrElse NotFound
|
||||||
val name = repository.name
|
}
|
||||||
|
})
|
||||||
issues.html.create(
|
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||||
getMilestones(owner, name),
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
getLabels(owner, name),
|
issues.html.create(
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
repository)
|
getMilestones(owner, name),
|
||||||
})
|
getLabels(owner, name),
|
||||||
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
repository)
|
||||||
val owner = repository.owner
|
}
|
||||||
val name = repository.name
|
})
|
||||||
val writable = hasWritePermission(owner, name, context.loginAccount)
|
|
||||||
|
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
||||||
val issueId = createIssue(owner, name, context.loginAccount.get.userName, form.title, form.content,
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
if(writable) form.assignedUserName else None,
|
val writable = hasWritePermission(owner, name, context.loginAccount)
|
||||||
if(writable) form.milestoneId else None)
|
val userName = context.loginAccount.get.userName
|
||||||
|
|
||||||
if(writable){
|
// insert issue
|
||||||
form.labelNames.map { value =>
|
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
||||||
val labels = getLabels(owner, name)
|
if(writable) form.assignedUserName else None,
|
||||||
value.split(",").foreach { labelName =>
|
if(writable) form.milestoneId else None)
|
||||||
labels.find(_.labelName == labelName).map { label =>
|
|
||||||
registerIssueLabel(owner, name, issueId, label.labelId)
|
// insert labels
|
||||||
}
|
if(writable){
|
||||||
}
|
form.labelNames.map { value =>
|
||||||
}
|
val labels = getLabels(owner, name)
|
||||||
}
|
value.split(",").foreach { labelName =>
|
||||||
|
labels.find(_.labelName == labelName).map { label =>
|
||||||
redirect("/%s/%s/issues/%d".format(owner, name, issueId))
|
registerIssueLabel(owner, name, issueId, label.labelId)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
}
|
||||||
val owner = repository.owner
|
}
|
||||||
val name = repository.name
|
|
||||||
|
// record activity
|
||||||
getIssue(owner, name, params("id")).map { issue =>
|
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
||||||
if(isEditable(owner, name, issue.openedUserName)){
|
|
||||||
updateIssue(owner, name, issue.issueId, form.title, form.content)
|
// extract references and create refer comment
|
||||||
redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId))
|
getIssue(owner, name, issueId.toString).foreach { issue =>
|
||||||
} else Unauthorized
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||||
} getOrElse NotFound
|
}
|
||||||
})
|
|
||||||
|
// notifications
|
||||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||||
val owner = repository.owner
|
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||||
val name = repository.name
|
}
|
||||||
|
|
||||||
getIssue(owner, name, form.issueId.toString).map { issue =>
|
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||||
redirect("/%s/%s/issues/%d#comment-%d".format(
|
}
|
||||||
owner, name, form.issueId,
|
})
|
||||||
createComment(owner, name, context.loginAccount.get.userName,
|
|
||||||
form.issueId,
|
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
||||||
form.content,
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
if(isEditable(owner, name, issue.openedUserName)){
|
getIssue(owner, name, params("id")).map { issue =>
|
||||||
params.get("action") filter { action =>
|
if(isEditable(owner, name, issue.openedUserName)){
|
||||||
updateClosed(owner, name, form.issueId, if(action == "close") true else false) > 0
|
// update issue
|
||||||
}
|
updateIssue(owner, name, issue.issueId, form.title, form.content)
|
||||||
} else None)
|
// extract references and create refer comment
|
||||||
))
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||||
}
|
|
||||||
})
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
|
} else Unauthorized
|
||||||
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
} getOrElse NotFound
|
||||||
val owner = repository.owner
|
}
|
||||||
val name = repository.name
|
})
|
||||||
|
|
||||||
getComment(owner, name, params("id")).map { comment =>
|
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
if(isEditable(owner, name, comment.commentedUserName)){
|
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
|
||||||
updateComment(comment.commentId, form.content)
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId))
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
} else Unauthorized
|
} getOrElse NotFound
|
||||||
} getOrElse NotFound
|
})
|
||||||
})
|
|
||||||
|
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
||||||
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
|
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
||||||
getIssue(repository.owner, repository.name, params("id")) map { x =>
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
params.get("dataType") collect {
|
} getOrElse NotFound
|
||||||
case t if t == "html" => issues.html.editissue(
|
})
|
||||||
x.title, x.content, x.issueId, x.userName, x.repositoryName)
|
|
||||||
} getOrElse {
|
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
contentType = formats("json")
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
org.json4s.jackson.Serialization.write(
|
getComment(owner, name, params("id")).map { comment =>
|
||||||
Map("title" -> x.title,
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
|
updateComment(comment.commentId, form.content)
|
||||||
repository, false, true, true)
|
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
||||||
))
|
} else Unauthorized
|
||||||
}
|
} getOrElse NotFound
|
||||||
} else Unauthorized
|
}
|
||||||
} getOrElse NotFound
|
})
|
||||||
})
|
|
||||||
|
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
|
||||||
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
getComment(repository.owner, repository.name, params("id")) map { x =>
|
getComment(owner, name, params("id")).map { comment =>
|
||||||
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
params.get("dataType") collect {
|
Ok(deleteComment(comment.commentId))
|
||||||
case t if t == "html" => issues.html.editcomment(
|
} else Unauthorized
|
||||||
x.content, x.commentId, x.userName, x.repositoryName)
|
} getOrElse NotFound
|
||||||
} getOrElse {
|
}
|
||||||
contentType = formats("json")
|
})
|
||||||
org.json4s.jackson.Serialization.write(
|
|
||||||
Map("content" -> view.Markdown.toHtml(x.content,
|
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
|
||||||
repository, false, true, true)
|
getIssue(repository.owner, repository.name, params("id")) map { x =>
|
||||||
))
|
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
|
||||||
}
|
params.get("dataType") collect {
|
||||||
} else Unauthorized
|
case t if t == "html" => issues.html.editissue(
|
||||||
} getOrElse NotFound
|
x.title, x.content, x.issueId, x.userName, x.repositoryName)
|
||||||
})
|
} getOrElse {
|
||||||
|
contentType = formats("json")
|
||||||
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
org.json4s.jackson.Serialization.write(
|
||||||
val issueId = params("id").toInt
|
Map("title" -> x.title,
|
||||||
|
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
|
||||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
repository, false, true)
|
||||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
))
|
||||||
})
|
}
|
||||||
|
} else Unauthorized
|
||||||
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
} getOrElse NotFound
|
||||||
val issueId = params("id").toInt
|
})
|
||||||
|
|
||||||
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
|
||||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
getComment(repository.owner, repository.name, params("id")) map { x =>
|
||||||
})
|
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
||||||
|
params.get("dataType") collect {
|
||||||
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
case t if t == "html" => issues.html.editcomment(
|
||||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt,
|
x.content, x.commentId, x.userName, x.repositoryName)
|
||||||
params.get("assignedUserName") filter (_.trim != ""))
|
} getOrElse {
|
||||||
Ok("updated")
|
contentType = formats("json")
|
||||||
})
|
org.json4s.jackson.Serialization.write(
|
||||||
|
Map("content" -> view.Markdown.toHtml(x.content,
|
||||||
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
|
repository, false, true)
|
||||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt,
|
))
|
||||||
params.get("milestoneId") collect { case x if x.trim != "" => x.toInt })
|
}
|
||||||
Ok("updated")
|
} else Unauthorized
|
||||||
})
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
|
||||||
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
||||||
|
defining(params("id").toInt){ issueId =>
|
||||||
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||||
val owner = repository.owner
|
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||||
val repoName = repository.name
|
}
|
||||||
val userName = if(filter != "all") Some(params("userName")) else None
|
})
|
||||||
val sessionKey = "%s/%s/issues".format(owner, repoName)
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
||||||
val page = try {
|
defining(params("id").toInt){ issueId =>
|
||||||
val i = params.getOrElse("page", "1").toInt
|
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||||
if(i <= 0) 1 else i
|
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||||
} catch {
|
}
|
||||||
case e: NumberFormatException => 1
|
})
|
||||||
}
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
||||||
// retrieve search condition
|
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
|
||||||
val condition = if(request.getQueryString == null){
|
Ok("updated")
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
})
|
||||||
} else IssueSearchCondition(request)
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
|
||||||
session.put(sessionKey, condition)
|
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
|
||||||
|
milestoneId("milestoneId").map { milestoneId =>
|
||||||
issues.html.list(
|
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||||
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
|
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
|
||||||
page,
|
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
|
||||||
getLabels(owner, repoName),
|
} getOrElse NotFound
|
||||||
getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
|
} getOrElse Ok()
|
||||||
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
|
})
|
||||||
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
|
|
||||||
countIssue(owner, repoName, condition, "all", None),
|
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||||
context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))),
|
defining(params.get("value")){ action =>
|
||||||
context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))),
|
executeBatch(repository) {
|
||||||
countIssueGroupByLabels(owner, repoName, condition, filter, userName),
|
handleComment(_, None, repository)( _ => action)
|
||||||
condition,
|
}
|
||||||
filter,
|
}
|
||||||
repository,
|
})
|
||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
|
||||||
}
|
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
|
||||||
|
params("value").toIntOpt.map{ labelId =>
|
||||||
}
|
executeBatch(repository) { issueId =>
|
||||||
|
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
||||||
|
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
|
||||||
|
defining(assignedUserName("value")){ value =>
|
||||||
|
executeBatch(repository) {
|
||||||
|
updateAssignedUserName(repository.owner, repository.name, _, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
|
||||||
|
defining(milestoneId("value")){ value =>
|
||||||
|
executeBatch(repository) {
|
||||||
|
updateMilestoneId(repository.owner, repository.name, _, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
|
||||||
|
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
||||||
|
case dir if(dir.exists && dir.isDirectory) =>
|
||||||
|
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
||||||
|
contentType = FileUtil.getMimeType(file.getName)
|
||||||
|
file
|
||||||
|
}
|
||||||
|
case _ => None
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||||
|
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
||||||
|
|
||||||
|
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||||
|
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||||
|
|
||||||
|
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
|
||||||
|
params("checked").split(',') map(_.toInt) foreach execute
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||||
|
}
|
||||||
|
|
||||||
|
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||||
|
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
|
||||||
|
fromIssue.issueId + ":" + fromIssue.title, "refer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||||
|
*/
|
||||||
|
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
||||||
|
(getAction: model.Issue => Option[String] =
|
||||||
|
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
||||||
|
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
|
||||||
|
getIssue(owner, name, issueId.toString) map { issue =>
|
||||||
|
val (action, recordActivity) =
|
||||||
|
getAction(issue)
|
||||||
|
.collect {
|
||||||
|
case "close" => true -> (Some("close") ->
|
||||||
|
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||||
|
case "reopen" => false -> (Some("reopen") ->
|
||||||
|
Some(recordReopenIssueActivity _))
|
||||||
|
}
|
||||||
|
.map { case (closed, t) =>
|
||||||
|
updateClosed(owner, name, issueId, closed)
|
||||||
|
t
|
||||||
|
}
|
||||||
|
.getOrElse(None -> None)
|
||||||
|
|
||||||
|
val commentId = content
|
||||||
|
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
|
||||||
|
.getOrElse ( action.get.capitalize -> action.get )
|
||||||
|
match {
|
||||||
|
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
content foreach {
|
||||||
|
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||||
|
(owner, name, userName, issueId, _)
|
||||||
|
}
|
||||||
|
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
||||||
|
|
||||||
|
// extract references and create refer comment
|
||||||
|
content.map { content =>
|
||||||
|
createReferComment(owner, name, issue, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
Notifier() match {
|
||||||
|
case f =>
|
||||||
|
content foreach {
|
||||||
|
f.toNotify(repository, issueId, _){
|
||||||
|
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action foreach {
|
||||||
|
f.toNotify(repository, issueId, _){
|
||||||
|
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issue -> commentId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
||||||
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||||
|
val filterUser = Map(filter -> params.getOrElse("userName", ""))
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
val sessionKey = Keys.Session.Issues(owner, repoName)
|
||||||
|
|
||||||
|
// retrieve search condition
|
||||||
|
val condition = session.putAndGet(sessionKey,
|
||||||
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
|
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||||
|
)
|
||||||
|
|
||||||
|
issues.html.list(
|
||||||
|
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||||
|
page,
|
||||||
|
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||||
|
getMilestones(owner, repoName),
|
||||||
|
getLabels(owner, repoName),
|
||||||
|
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
|
||||||
|
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
|
||||||
|
countIssue(condition, Map.empty, false, owner -> repoName),
|
||||||
|
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
|
||||||
|
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
|
||||||
|
countIssueGroupByLabels(owner, repoName, condition, filterUser),
|
||||||
|
condition,
|
||||||
|
filter,
|
||||||
|
repository,
|
||||||
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package app
|
|||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import service._
|
import service._
|
||||||
import util.CollaboratorsAuthenticator
|
import util.CollaboratorsAuthenticator
|
||||||
|
import util.Implicits._
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class LabelsController extends LabelsControllerBase
|
class LabelsController extends LabelsControllerBase
|
||||||
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
|
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
|
||||||
@@ -13,18 +15,18 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
case class LabelForm(labelName: String, color: String)
|
case class LabelForm(labelName: String, color: String)
|
||||||
|
|
||||||
val newForm = mapping(
|
val newForm = mapping(
|
||||||
"newLabelName" -> trim(label("Label name", text(required, identifier, maxlength(100)))),
|
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||||
"newColor" -> trim(label("Color", text(required, color)))
|
"newColor" -> trim(label("Color", text(required, color)))
|
||||||
)(LabelForm.apply)
|
)(LabelForm.apply)
|
||||||
|
|
||||||
val editForm = mapping(
|
val editForm = mapping(
|
||||||
"editLabelName" -> trim(label("Label name", text(required, identifier, maxlength(100)))),
|
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||||
"editColor" -> trim(label("Color", text(required, color)))
|
"editColor" -> trim(label("Color", text(required, color)))
|
||||||
)(LabelForm.apply)
|
)(LabelForm.apply)
|
||||||
|
|
||||||
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||||
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
||||||
redirect("/%s/%s/issues".format(repository.owner, repository.name))
|
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
|
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
|
||||||
@@ -47,4 +49,18 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* Constraint for the identifier such as user name, repository name or page name.
|
||||||
|
*/
|
||||||
|
private def labelName: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
if(value.contains(',')){
|
||||||
|
Some(s"${name} contains invalid character.")
|
||||||
|
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||||
|
Some(s"${name} starts with invalid character.")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package app
|
|||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
|
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||||
|
import util.Implicits._
|
||||||
|
|
||||||
class MilestonesController extends MilestonesControllerBase
|
class MilestonesController extends MilestonesControllerBase
|
||||||
with MilestonesService with RepositoryService with AccountService
|
with MilestonesService with RepositoryService with AccountService
|
||||||
@@ -35,38 +36,48 @@ trait MilestonesControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
||||||
createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate)
|
createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate)
|
||||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
|
||||||
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, params("milestoneId").toInt), repository)
|
params("milestoneId").toIntOpt.map{ milestoneId =>
|
||||||
|
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
|
||||||
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.repository))
|
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
closeMilestone(milestone)
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
closeMilestone(milestone)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
openMilestone(milestone)
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
openMilestone(milestone)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
|
||||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
|
||||||
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
479
src/main/scala/app/PullRequestsController.scala
Normal file
479
src/main/scala/app/PullRequestsController.scala
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
|
||||||
|
import util.Directory._
|
||||||
|
import util.Implicits._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import service._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.eclipse.jgit.transport.RefSpec
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||||
|
import service.IssuesService._
|
||||||
|
import service.PullRequestService._
|
||||||
|
import util.JGitUtil.DiffInfo
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.eclipse.jgit.merge.MergeStrategy
|
||||||
|
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||||
|
import service.WebHookService.WebHookPayload
|
||||||
|
|
||||||
|
class PullRequestsController extends PullRequestsControllerBase
|
||||||
|
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||||
|
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
|
trait PullRequestsControllerBase extends ControllerBase {
|
||||||
|
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||||
|
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||||
|
|
||||||
|
val pullRequestForm = mapping(
|
||||||
|
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||||
|
"content" -> trim(label("Content", optional(text()))),
|
||||||
|
"targetUserName" -> trim(text(required, maxlength(100))),
|
||||||
|
"targetBranch" -> trim(text(required, maxlength(100))),
|
||||||
|
"requestUserName" -> trim(text(required, maxlength(100))),
|
||||||
|
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
||||||
|
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||||
|
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||||
|
"commitIdTo" -> trim(text(required, maxlength(40)))
|
||||||
|
)(PullRequestForm.apply)
|
||||||
|
|
||||||
|
val mergeForm = mapping(
|
||||||
|
"message" -> trim(label("Message", text(required)))
|
||||||
|
)(MergeForm.apply)
|
||||||
|
|
||||||
|
case class PullRequestForm(
|
||||||
|
title: String,
|
||||||
|
content: Option[String],
|
||||||
|
targetUserName: String,
|
||||||
|
targetBranch: String,
|
||||||
|
requestUserName: String,
|
||||||
|
requestRepositoryName: String,
|
||||||
|
requestBranch: String,
|
||||||
|
commitIdFrom: String,
|
||||||
|
commitIdTo: String)
|
||||||
|
|
||||||
|
case class MergeForm(message: String)
|
||||||
|
|
||||||
|
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||||
|
searchPullRequests(None, repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
|
||||||
|
searchPullRequests(Some(params("userName")), repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||||
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
|
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||||
|
val (commits, diffs) =
|
||||||
|
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
||||||
|
|
||||||
|
pulls.html.pullreq(
|
||||||
|
issue, pullreq,
|
||||||
|
getComments(owner, name, issueId),
|
||||||
|
getIssueLabels(owner, name, issueId),
|
||||||
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
|
getMilestonesWithIssueCount(owner, name),
|
||||||
|
getLabels(owner, name),
|
||||||
|
commits,
|
||||||
|
diffs,
|
||||||
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
|
repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
||||||
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
|
pulls.html.mergeguide(
|
||||||
|
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
|
||||||
|
pullreq,
|
||||||
|
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
|
||||||
|
params("id").toIntOpt.map { issueId =>
|
||||||
|
val branchName = multiParams("splat").head
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
if(repository.repository.defaultBranch != branchName){
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
git.branchDelete().setForce(true).setBranchNames(branchName).call()
|
||||||
|
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||||
|
params("id").toIntOpt.flatMap { issueId =>
|
||||||
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
LockUtil.lock(s"${owner}/${name}"){
|
||||||
|
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||||
|
using(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||||
|
// mark issue as merged and close.
|
||||||
|
val loginAccount = context.loginAccount.get
|
||||||
|
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||||
|
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||||
|
updateClosed(owner, name, issueId, true)
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||||
|
|
||||||
|
// merge
|
||||||
|
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
|
||||||
|
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||||
|
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
|
||||||
|
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||||
|
val conflicted = try {
|
||||||
|
!merger.merge(mergeBaseTip, mergeTip)
|
||||||
|
} catch {
|
||||||
|
case e: NoMergeBaseException => true
|
||||||
|
}
|
||||||
|
if (conflicted) {
|
||||||
|
throw new RuntimeException("This pull request can't merge automatically.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates merge commit
|
||||||
|
val mergeCommit = new CommitBuilder()
|
||||||
|
mergeCommit.setTreeId(merger.getResultTreeId)
|
||||||
|
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
||||||
|
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||||
|
mergeCommit.setAuthor(personIdent)
|
||||||
|
mergeCommit.setCommitter(personIdent)
|
||||||
|
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" +
|
||||||
|
form.message)
|
||||||
|
|
||||||
|
// insertObject and got mergeCommit Object Id
|
||||||
|
val inserter = git.getRepository.newObjectInserter
|
||||||
|
val mergeCommitId = inserter.insert(mergeCommit)
|
||||||
|
inserter.flush()
|
||||||
|
inserter.release()
|
||||||
|
|
||||||
|
// update refs
|
||||||
|
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
|
||||||
|
refUpdate.setNewObjectId(mergeCommitId)
|
||||||
|
refUpdate.setForceUpdate(false)
|
||||||
|
refUpdate.setRefLogIdent(personIdent)
|
||||||
|
refUpdate.setRefLogMessage("merged", true)
|
||||||
|
refUpdate.update()
|
||||||
|
|
||||||
|
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||||
|
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||||
|
|
||||||
|
// close issue by content of pull request
|
||||||
|
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
|
||||||
|
if(pullreq.branch == defaultBranch){
|
||||||
|
commits.flatten.foreach { commit =>
|
||||||
|
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||||
|
}
|
||||||
|
issue.content match {
|
||||||
|
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||||
|
}
|
||||||
|
// call web hook
|
||||||
|
getWebHookURLs(owner, name) match {
|
||||||
|
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||||
|
for(ownerAccount <- getAccountByUserName(owner)){
|
||||||
|
callWebHook(owner, name, webHookURLs,
|
||||||
|
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
Notifier().toNotify(repository, issueId, "merge"){
|
||||||
|
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||||
|
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||||
|
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||||
|
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
||||||
|
using(
|
||||||
|
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||||
|
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||||
|
){ (oldGit, newGit) =>
|
||||||
|
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
||||||
|
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
||||||
|
|
||||||
|
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
case _ => {
|
||||||
|
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
|
||||||
|
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||||
|
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||||
|
} getOrElse {
|
||||||
|
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||||
|
val Seq(origin, forked) = multiParams("splat")
|
||||||
|
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||||
|
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||||
|
|
||||||
|
(for(
|
||||||
|
originRepositoryName <- if(originOwner == forkedOwner){
|
||||||
|
Some(forkedRepository.name)
|
||||||
|
} else {
|
||||||
|
forkedRepository.repository.originRepositoryName.orElse {
|
||||||
|
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||||
|
) yield {
|
||||||
|
using(
|
||||||
|
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||||
|
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||||
|
){ case (oldGit, newGit) =>
|
||||||
|
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||||
|
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||||
|
|
||||||
|
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||||
|
originRepository.owner, originRepository.name, originBranch,
|
||||||
|
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||||
|
|
||||||
|
val oldId = oldGit.getRepository.resolve(forkedId)
|
||||||
|
val newId = newGit.getRepository.resolve(forkedBranch)
|
||||||
|
|
||||||
|
val (commits, diffs) = getRequestCompareInfo(
|
||||||
|
originRepository.owner, originRepository.name, oldId.getName,
|
||||||
|
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||||
|
|
||||||
|
pulls.html.compare(
|
||||||
|
commits,
|
||||||
|
diffs,
|
||||||
|
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||||
|
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||||
|
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||||
|
},
|
||||||
|
originBranch,
|
||||||
|
forkedBranch,
|
||||||
|
oldId.getName,
|
||||||
|
newId.getName,
|
||||||
|
forkedRepository,
|
||||||
|
originRepository,
|
||||||
|
forkedRepository,
|
||||||
|
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
|
||||||
|
val Seq(origin, forked) = multiParams("splat")
|
||||||
|
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||||
|
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||||
|
|
||||||
|
(for(
|
||||||
|
originRepositoryName <- if(originOwner == forkedOwner){
|
||||||
|
Some(forkedRepository.name)
|
||||||
|
} else {
|
||||||
|
forkedRepository.repository.originRepositoryName.orElse {
|
||||||
|
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||||
|
) yield {
|
||||||
|
using(
|
||||||
|
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||||
|
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||||
|
){ case (oldGit, newGit) =>
|
||||||
|
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||||
|
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||||
|
|
||||||
|
pulls.html.mergecheck(
|
||||||
|
checkConflict(originRepository.owner, originRepository.name, originBranch,
|
||||||
|
forkedRepository.owner, forkedRepository.name, forkedBranch))
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||||
|
val loginUserName = context.loginAccount.get.userName
|
||||||
|
|
||||||
|
val issueId = createIssue(
|
||||||
|
owner = repository.owner,
|
||||||
|
repository = repository.name,
|
||||||
|
loginUser = loginUserName,
|
||||||
|
title = form.title,
|
||||||
|
content = form.content,
|
||||||
|
assignedUserName = None,
|
||||||
|
milestoneId = None,
|
||||||
|
isPullRequest = true)
|
||||||
|
|
||||||
|
createPullRequest(
|
||||||
|
originUserName = repository.owner,
|
||||||
|
originRepositoryName = repository.name,
|
||||||
|
issueId = issueId,
|
||||||
|
originBranch = form.targetBranch,
|
||||||
|
requestUserName = form.requestUserName,
|
||||||
|
requestRepositoryName = form.requestRepositoryName,
|
||||||
|
requestBranch = form.requestBranch,
|
||||||
|
commitIdFrom = form.commitIdFrom,
|
||||||
|
commitIdTo = form.commitIdTo)
|
||||||
|
|
||||||
|
// fetch requested branch
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
git.fetch
|
||||||
|
.setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
|
||||||
|
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
|
||||||
|
.call
|
||||||
|
}
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||||
|
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||||
|
*/
|
||||||
|
private def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||||
|
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||||
|
LockUtil.lock(s"${userName}/${repositoryName}"){
|
||||||
|
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||||
|
val remoteRefName = s"refs/heads/${branch}"
|
||||||
|
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||||
|
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||||
|
try {
|
||||||
|
// fetch objects from origin repository branch
|
||||||
|
git.fetch
|
||||||
|
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||||
|
.setRefSpecs(refSpec)
|
||||||
|
.call
|
||||||
|
|
||||||
|
// merge conflict check
|
||||||
|
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||||
|
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
|
||||||
|
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||||
|
try {
|
||||||
|
!merger.merge(mergeBaseTip, mergeTip)
|
||||||
|
} catch {
|
||||||
|
case e: NoMergeBaseException => true
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||||
|
refUpdate.setForceUpdate(true)
|
||||||
|
refUpdate.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
|
||||||
|
*/
|
||||||
|
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
|
||||||
|
requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||||
|
issueId: Int): Boolean = {
|
||||||
|
LockUtil.lock(s"${userName}/${repositoryName}") {
|
||||||
|
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||||
|
// merge
|
||||||
|
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||||
|
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||||
|
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||||
|
try {
|
||||||
|
!merger.merge(mergeBaseTip, mergeTip)
|
||||||
|
} catch {
|
||||||
|
case e: NoMergeBaseException => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||||
|
*
|
||||||
|
* - "owner:branch" to ("owner", "branch")
|
||||||
|
* - "branch" to ("defaultOwner", "branch")
|
||||||
|
*/
|
||||||
|
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
|
||||||
|
if(value.contains(':')){
|
||||||
|
val array = value.split(":")
|
||||||
|
(array(0), array(1))
|
||||||
|
} else {
|
||||||
|
(defaultOwner, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
||||||
|
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
|
||||||
|
using(
|
||||||
|
Git.open(getRepositoryDir(userName, repositoryName)),
|
||||||
|
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
|
||||||
|
){ (oldGit, newGit) =>
|
||||||
|
val oldId = oldGit.getRepository.resolve(branch)
|
||||||
|
val newId = newGit.getRepository.resolve(requestCommitId)
|
||||||
|
|
||||||
|
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
|
||||||
|
new CommitInfo(revCommit)
|
||||||
|
}.toList.splitWith { (commit1, commit2) =>
|
||||||
|
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
|
||||||
|
|
||||||
|
(commits, diffs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
|
||||||
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||||
|
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
val sessionKey = Keys.Session.Pulls(owner, repoName)
|
||||||
|
|
||||||
|
// retrieve search condition
|
||||||
|
val condition = session.putAndGet(sessionKey,
|
||||||
|
if(request.hasQueryString) IssueSearchCondition(request)
|
||||||
|
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||||
|
)
|
||||||
|
|
||||||
|
pulls.html.list(
|
||||||
|
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||||
|
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
|
||||||
|
userName,
|
||||||
|
page,
|
||||||
|
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
|
||||||
|
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
|
||||||
|
countIssue(condition, Map.empty, true, owner -> repoName),
|
||||||
|
condition,
|
||||||
|
repository,
|
||||||
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
272
src/main/scala/app/RepositorySettingsController.scala
Normal file
272
src/main/scala/app/RepositorySettingsController.scala
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import service._
|
||||||
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Implicits._
|
||||||
|
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
import service.WebHookService.WebHookPayload
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
import util.ControlUtil._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
|
||||||
|
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||||
|
with RepositoryService with AccountService with WebHookService
|
||||||
|
with OwnerAuthenticator with UsersAuthenticator
|
||||||
|
|
||||||
|
trait RepositorySettingsControllerBase extends ControllerBase {
|
||||||
|
self: RepositoryService with AccountService with WebHookService
|
||||||
|
with OwnerAuthenticator with UsersAuthenticator =>
|
||||||
|
|
||||||
|
// for repository options
|
||||||
|
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||||
|
|
||||||
|
val optionsForm = mapping(
|
||||||
|
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||||
|
"description" -> trim(label("Description" , optional(text()))),
|
||||||
|
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
||||||
|
"isPrivate" -> trim(label("Repository Type", boolean()))
|
||||||
|
)(OptionsForm.apply)
|
||||||
|
|
||||||
|
// for collaborator addition
|
||||||
|
case class CollaboratorForm(userName: String)
|
||||||
|
|
||||||
|
val collaboratorForm = mapping(
|
||||||
|
"userName" -> trim(label("Username", text(required, collaborator)))
|
||||||
|
)(CollaboratorForm.apply)
|
||||||
|
|
||||||
|
// for web hook url addition
|
||||||
|
case class WebHookForm(url: String)
|
||||||
|
|
||||||
|
val webHookForm = mapping(
|
||||||
|
"url" -> trim(label("url", text(required, webHook)))
|
||||||
|
)(WebHookForm.apply)
|
||||||
|
|
||||||
|
// for transfer ownership
|
||||||
|
case class TransferOwnerShipForm(newOwner: String)
|
||||||
|
|
||||||
|
val transferForm = mapping(
|
||||||
|
"newOwner" -> trim(label("New owner", text(required, transferUser)))
|
||||||
|
)(TransferOwnerShipForm.apply)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the Options page.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings")(ownerOnly { repository =>
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the Options page.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/options")(ownerOnly {
|
||||||
|
settings.html.options(_, flash.get("info"))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the repository options.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
||||||
|
saveRepositoryOptions(
|
||||||
|
repository.owner,
|
||||||
|
repository.name,
|
||||||
|
form.description,
|
||||||
|
if(repository.branchList.isEmpty) "master" else form.defaultBranch,
|
||||||
|
repository.repository.parentUserName.map { _ =>
|
||||||
|
repository.repository.isPrivate
|
||||||
|
} getOrElse form.isPrivate
|
||||||
|
)
|
||||||
|
// Change repository name
|
||||||
|
if(repository.name != form.repositoryName){
|
||||||
|
// Update database
|
||||||
|
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
|
||||||
|
// Move git repository
|
||||||
|
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||||
|
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
|
||||||
|
}
|
||||||
|
// Move wiki repository
|
||||||
|
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||||
|
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flash += "info" -> "Repository settings has been updated."
|
||||||
|
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the Collaborators page.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
|
||||||
|
settings.html.collaborators(
|
||||||
|
getCollaborators(repository.owner, repository.name),
|
||||||
|
getAccountByUserName(repository.owner).get.isGroupAccount,
|
||||||
|
repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the collaborator.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
|
||||||
|
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||||
|
addCollaborator(repository.owner, repository.name, form.userName)
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the collaborator.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
|
||||||
|
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||||
|
removeCollaborator(repository.owner, repository.name, params("name"))
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the web hook page.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
|
||||||
|
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the web hook URL.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
|
||||||
|
addWebHookURL(repository.owner, repository.name, form.url)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the web hook URL.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
|
||||||
|
deleteWebHookURL(repository.owner, repository.name, params("url"))
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the test request to registered web hook URLs.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
val commits = git.log
|
||||||
|
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||||
|
.setMaxCount(3)
|
||||||
|
.call.iterator.asScala.map(new CommitInfo(_))
|
||||||
|
|
||||||
|
getWebHookURLs(repository.owner, repository.name) match {
|
||||||
|
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||||
|
for(ownerAccount <- getAccountByUserName(repository.owner)){
|
||||||
|
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||||
|
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
|
||||||
|
flash += "info" -> "Test payload deployed!"
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the danger zone.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/danger")(ownerOnly {
|
||||||
|
settings.html.danger(_)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transfer repository ownership.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
|
||||||
|
// Change repository owner
|
||||||
|
if(repository.owner != form.newOwner){
|
||||||
|
LockUtil.lock(s"${repository.owner}/${repository.name}"){
|
||||||
|
// Update database
|
||||||
|
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
|
||||||
|
// Move git repository
|
||||||
|
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||||
|
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
|
||||||
|
}
|
||||||
|
// Move wiki repository
|
||||||
|
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||||
|
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redirect(s"/${form.newOwner}/${repository.name}")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the repository.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
|
||||||
|
LockUtil.lock(s"${repository.owner}/${repository.name}"){
|
||||||
|
deleteRepository(repository.owner, repository.name)
|
||||||
|
|
||||||
|
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
|
||||||
|
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
|
||||||
|
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides duplication check for web hook url.
|
||||||
|
*/
|
||||||
|
private def webHook: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Constraint to validate the collaborator name.
|
||||||
|
*/
|
||||||
|
private def collaborator: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
getAccountByUserName(value) match {
|
||||||
|
case None => Some("User does not exist.")
|
||||||
|
case Some(x) if(x.isGroupAccount)
|
||||||
|
=> Some("User does not exist.")
|
||||||
|
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
|
||||||
|
=> Some("User can access this repository already.")
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate check for the rename repository name.
|
||||||
|
*/
|
||||||
|
private def renameRepositoryName: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||||
|
params.get("repository").filter(_ != value).flatMap { _ =>
|
||||||
|
params.get("owner").flatMap { userName =>
|
||||||
|
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Constraint to validate the repository transfer user.
|
||||||
|
*/
|
||||||
|
private def transferUser: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
getAccountByUserName(value) match {
|
||||||
|
case None => Some("User does not exist.")
|
||||||
|
case Some(x) => if(x.userName == params("owner")){
|
||||||
|
Some("This is current repository owner.")
|
||||||
|
} else {
|
||||||
|
params.get("repository").flatMap { repositoryName =>
|
||||||
|
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,74 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
|
import _root_.util.JGitUtil.CommitInfo
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.Implicits._
|
import util.Implicits._
|
||||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil}
|
import _root_.util.ControlUtil._
|
||||||
|
import _root_.util._
|
||||||
import service._
|
import service._
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
|
import org.eclipse.jgit.api.{ArchiveCommand, Git}
|
||||||
|
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
|
||||||
import org.eclipse.jgit.lib._
|
import org.eclipse.jgit.lib._
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.eclipse.jgit.treewalk._
|
import org.eclipse.jgit.treewalk._
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
|
import service.WebHookService.WebHookPayload
|
||||||
|
|
||||||
|
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||||
|
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
|
||||||
|
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
|
||||||
with RepositoryService with AccountService with ReferrerAuthenticator
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository viewer.
|
* The repository viewer.
|
||||||
*/
|
*/
|
||||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with ReferrerAuthenticator =>
|
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
|
||||||
|
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
|
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||||
|
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||||
|
|
||||||
|
case class EditorForm(
|
||||||
|
branch: String,
|
||||||
|
path: String,
|
||||||
|
content: String,
|
||||||
|
message: Option[String],
|
||||||
|
charset: String,
|
||||||
|
lineSeparator: String,
|
||||||
|
newFileName: String,
|
||||||
|
oldFileName: Option[String]
|
||||||
|
)
|
||||||
|
|
||||||
|
case class DeleteForm(
|
||||||
|
branch: String,
|
||||||
|
path: String,
|
||||||
|
message: Option[String],
|
||||||
|
fileName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
val editorForm = mapping(
|
||||||
|
"branch" -> trim(label("Branch", text(required))),
|
||||||
|
"path" -> trim(label("Path", text())),
|
||||||
|
"content" -> trim(label("Content", text(required))),
|
||||||
|
"message" -> trim(label("Message", optional(text()))),
|
||||||
|
"charset" -> trim(label("Charset", text(required))),
|
||||||
|
"lineSeparator" -> trim(label("Line Separator", text(required))),
|
||||||
|
"newFileName" -> trim(label("Filename", text(required))),
|
||||||
|
"oldFileName" -> trim(label("Old filename", optional(text())))
|
||||||
|
)(EditorForm.apply)
|
||||||
|
|
||||||
|
val deleteForm = mapping(
|
||||||
|
"branch" -> trim(label("Branch", text(required))),
|
||||||
|
"path" -> trim(label("Path", text())),
|
||||||
|
"message" -> trim(label("Message", optional(text()))),
|
||||||
|
"fileName" -> trim(label("Filename", text(required)))
|
||||||
|
)(DeleteForm.apply)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns converted HTML from Markdown for preview.
|
* Returns converted HTML from Markdown for preview.
|
||||||
@@ -27,8 +77,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
contentType = "text/html"
|
contentType = "text/html"
|
||||||
view.helpers.markdown(params("content"), repository,
|
view.helpers.markdown(params("content"), repository,
|
||||||
params("enableWikiLink").toBoolean,
|
params("enableWikiLink").toBoolean,
|
||||||
params("enableCommitLink").toBoolean,
|
params("enableRefsLink").toBoolean)
|
||||||
params("enableIssueLink").toBoolean)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,213 +86,357 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
get("/:owner/:repository")(referrersOnly {
|
get("/:owner/:repository")(referrersOnly {
|
||||||
fileList(_)
|
fileList(_)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the file list of the repository root and the specified branch.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/tree/:id")(referrersOnly {
|
|
||||||
fileList(_, params("id"))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the file list of the specified path and branch.
|
* Displays the file list of the specified path and branch.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/tree/:id/*")(referrersOnly {
|
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
|
||||||
fileList(_, params("id"), multiParams("splat").head)
|
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||||
})
|
if(path.isEmpty){
|
||||||
|
fileList(repository, id)
|
||||||
/**
|
} else {
|
||||||
* Displays the commit list of the specified branch.
|
fileList(repository, id, path)
|
||||||
*/
|
|
||||||
get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
|
|
||||||
val branchName = params("branch")
|
|
||||||
val page = params.getOrElse("page", "1").toInt
|
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30)
|
|
||||||
|
|
||||||
repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
|
|
||||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
|
||||||
}, page, hasNext)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the commit list of the specified resource.
|
* Displays the commit list of the specified resource.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository =>
|
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
|
||||||
val branchName = params("branch")
|
val (branchName, path) = splitPath(repository, multiParams("splat").head)
|
||||||
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
|
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
|
||||||
val page = params.getOrElse("page", "1").toInt
|
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30, path)
|
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
||||||
|
case Right((logs, hasNext)) =>
|
||||||
repo.html.commits(path.split("/").toList, branchName, repository,
|
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
||||||
logs.splitWith{ (commit1, commit2) =>
|
logs.splitWith{ (commit1, commit2) =>
|
||||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||||
}, page, hasNext)
|
}, page, hasNext)
|
||||||
|
case Left(_) => NotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||||
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
|
||||||
|
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
|
||||||
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||||
|
|
||||||
|
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||||
|
val paths = path.split("/")
|
||||||
|
repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
|
||||||
|
JGitUtil.getContentInfo(git, path, objectId))
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
|
||||||
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||||
|
|
||||||
|
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||||
|
val paths = path.split("/")
|
||||||
|
repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
|
||||||
|
JGitUtil.getContentInfo(git, path, objectId))
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
|
||||||
|
commitFile(repository, form.branch, form.path, Some(form.newFileName), None,
|
||||||
|
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
|
||||||
|
form.message.getOrElse(s"Create ${form.newFileName}"))
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||||
|
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
||||||
|
}")
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
|
||||||
|
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName,
|
||||||
|
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
|
||||||
|
if(form.oldFileName.exists(_ == form.newFileName)){
|
||||||
|
form.message.getOrElse(s"Update ${form.newFileName}")
|
||||||
|
} else {
|
||||||
|
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
|
||||||
|
})
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||||
|
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
||||||
|
}")
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
|
||||||
|
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
|
||||||
|
form.message.getOrElse(s"Delete ${form.fileName}"))
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the file content of the specified branch or commit.
|
* Displays the file content of the specified branch or commit.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/blob/:id/*")(referrersOnly { repository =>
|
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
||||||
val id = params("id") // branch name or commit id
|
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||||
val raw = params.get("raw").getOrElse("false").toBoolean
|
val raw = params.get("raw").getOrElse("false").toBoolean
|
||||||
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
|
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||||
|
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||||
@scala.annotation.tailrec
|
if(raw){
|
||||||
def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match {
|
// Download
|
||||||
case true if(walk.getPathString == path) => walk.getObjectId(0)
|
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
|
||||||
case true => getPathObjectId(path, walk)
|
contentType = FileUtil.getContentType(path, bytes)
|
||||||
}
|
bytes
|
||||||
|
|
||||||
val treeWalk = new TreeWalk(git.getRepository)
|
|
||||||
val objectId = try {
|
|
||||||
treeWalk.addTree(revCommit.getTree)
|
|
||||||
treeWalk.setRecursive(true)
|
|
||||||
getPathObjectId(path, treeWalk)
|
|
||||||
} finally {
|
|
||||||
treeWalk.release
|
|
||||||
}
|
|
||||||
|
|
||||||
if(raw){
|
|
||||||
// Download
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
JGitUtil.getContent(git, objectId, false).get
|
|
||||||
} else {
|
|
||||||
// Viewer
|
|
||||||
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
|
|
||||||
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
|
|
||||||
val bytes = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None
|
|
||||||
|
|
||||||
val content = if(viewer == "other"){
|
|
||||||
if(bytes.isDefined && FileUtil.isText(bytes.get)){
|
|
||||||
// text
|
|
||||||
JGitUtil.ContentInfo("text", bytes.map(new String(_, "UTF-8")))
|
|
||||||
} else {
|
|
||||||
// binary
|
|
||||||
JGitUtil.ContentInfo("binary", None)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// image or large
|
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
|
||||||
JGitUtil.ContentInfo(viewer, None)
|
new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
}
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays details of the specified commit.
|
* Displays details of the specified commit.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
||||||
val id = params("id")
|
val id = params("id")
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
|
||||||
|
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
||||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||||
repository, JGitUtil.getDiffs(git, id))
|
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||||
|
repository, diffs, oldCommitId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays branches.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
// retrieve latest update date of each branch
|
||||||
|
val branchInfo = repository.branchList.map { branchName =>
|
||||||
|
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
|
||||||
|
(branchName, revCommit.getCommitterIdent.getWhen)
|
||||||
|
}
|
||||||
|
repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes branch.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
|
||||||
|
val branchName = multiParams("splat").head
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
if(repository.repository.defaultBranch != branchName){
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
git.branchDelete().setForce(true).setBranchNames(branchName).call()
|
||||||
|
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/branches")
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays tags.
|
* Displays tags.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/tags")(referrersOnly {
|
get("/:owner/:repository/tags")(referrersOnly {
|
||||||
repo.html.tags(_)
|
repo.html.tags(_)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download repository contents as an archive.
|
* Download repository contents as an archive.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
|
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
|
||||||
val name = params("name")
|
multiParams("splat").head match {
|
||||||
|
case name if name.endsWith(".zip") =>
|
||||||
if(name.endsWith(".zip")){
|
archiveRepository(name, ".zip", repository)
|
||||||
val revision = name.replaceFirst("\\.zip$", "")
|
case name if name.endsWith(".tar.gz") =>
|
||||||
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
archiveRepository(name, ".tar.gz", repository)
|
||||||
if(workDir.exists){
|
case _ => BadRequest
|
||||||
FileUtils.deleteDirectory(workDir)
|
|
||||||
}
|
|
||||||
workDir.mkdirs
|
|
||||||
|
|
||||||
// clone the repository
|
|
||||||
val cloneDir = new File(workDir, revision)
|
|
||||||
JGitUtil.withGit(Git.cloneRepository
|
|
||||||
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
|
|
||||||
.setDirectory(cloneDir)
|
|
||||||
.call){ git =>
|
|
||||||
|
|
||||||
// checkout the specified revision
|
|
||||||
git.checkout.setName(revision).call
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove .git
|
|
||||||
FileUtils.deleteDirectory(new File(cloneDir, ".git"))
|
|
||||||
|
|
||||||
// create zip file
|
|
||||||
val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
|
|
||||||
FileUtil.createZipFile(zipFile, cloneDir)
|
|
||||||
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
zipFile
|
|
||||||
} else {
|
|
||||||
BadRequest
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/network/members")(referrersOnly { repository =>
|
||||||
|
repo.html.forked(
|
||||||
|
getRepository(
|
||||||
|
repository.repository.originUserName.getOrElse(repository.owner),
|
||||||
|
repository.repository.originRepositoryName.getOrElse(repository.name),
|
||||||
|
context.baseUrl),
|
||||||
|
getForkedRepositories(
|
||||||
|
repository.repository.originUserName.getOrElse(repository.owner),
|
||||||
|
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||||
|
repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
|
||||||
|
val id = repository.branchList.collectFirst {
|
||||||
|
case branch if(path == branch || path.startsWith(branch + "/")) => branch
|
||||||
|
} orElse repository.tags.collectFirst {
|
||||||
|
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
|
||||||
|
} getOrElse path.split("/")(0)
|
||||||
|
|
||||||
|
(id, path.substring(id.length).stripPrefix("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides HTML of the file list.
|
* Provides HTML of the file list.
|
||||||
*
|
*
|
||||||
* @param repository the repository information
|
* @param repository the repository information
|
||||||
* @param revstr the branch name or commit id(optional)
|
* @param revstr the branch name or commit id(optional)
|
||||||
* @param path the directory path (optional)
|
* @param path the directory path (optional)
|
||||||
* @return HTML of the file list
|
* @return HTML of the file list
|
||||||
*/
|
*/
|
||||||
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
|
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
|
||||||
getRepository(repository.owner, repository.name, baseUrl).map { repositoryInfo =>
|
if(repository.commitCount == 0){
|
||||||
val revision = if(revstr.isEmpty){
|
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
repositoryInfo.repository.defaultBranch
|
} else {
|
||||||
} else {
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
revstr
|
//val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
|
||||||
|
// get specified commit
|
||||||
|
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
|
||||||
|
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
|
||||||
|
// get files
|
||||||
|
val files = JGitUtil.getFileList(git, revision, path)
|
||||||
|
val parentPath = if (path == ".") Nil else path.split("/").toList
|
||||||
|
// process README.md or README.markdown
|
||||||
|
val readme = files.find { file =>
|
||||||
|
readmeFiles.contains(file.name.toLowerCase)
|
||||||
|
}.map { file =>
|
||||||
|
val path = (file.name :: parentPath.reverse).reverse
|
||||||
|
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
|
||||||
|
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.html.files(revision, repository,
|
||||||
|
if(path == ".") Nil else path.split("/").toList, // current path
|
||||||
|
new JGitUtil.CommitInfo(revCommit), // latest commit
|
||||||
|
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repositoryInfo.owner, repositoryInfo.name)){ git =>
|
private def commitFile(repository: service.RepositoryService.RepositoryInfo,
|
||||||
// get latest commit
|
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
content: String, charset: String, message: String) = {
|
||||||
|
|
||||||
val files = JGitUtil.getFileList(git, revision, path)
|
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
|
||||||
|
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
|
||||||
|
|
||||||
// process README.md
|
LockUtil.lock(s"${repository.owner}/${repository.name}"){
|
||||||
val readme = files.find(_.name == "README.md").map { file =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
new String(JGitUtil.getContent(Git.open(getRepositoryDir(repositoryInfo.owner, repositoryInfo.name)), file.id, true).get, "UTF-8")
|
val loginAccount = context.loginAccount.get
|
||||||
|
val builder = DirCache.newInCore.builder()
|
||||||
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headName = s"refs/heads/${branch}"
|
||||||
|
val headTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||||
|
|
||||||
|
JGitUtil.processTree(git, headTip){ (path, tree) =>
|
||||||
|
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.html.files(
|
newPath.foreach { newPath =>
|
||||||
// current branch
|
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
|
||||||
revision,
|
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
|
||||||
// repository
|
}
|
||||||
repositoryInfo,
|
builder.finish()
|
||||||
// current path
|
|
||||||
if(path == ".") Nil else path.split("/").toList,
|
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
|
||||||
// latest commit
|
loginAccount.fullName, loginAccount.mailAddress, message)
|
||||||
new JGitUtil.CommitInfo(revCommit),
|
|
||||||
// file list
|
inserter.flush()
|
||||||
files,
|
inserter.release()
|
||||||
// readme
|
|
||||||
readme
|
// update refs
|
||||||
)
|
val refUpdate = git.getRepository.updateRef(headName)
|
||||||
|
refUpdate.setNewObjectId(commitId)
|
||||||
|
refUpdate.setForceUpdate(false)
|
||||||
|
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||||
|
//refUpdate.setRefLogMessage("merged", true)
|
||||||
|
refUpdate.update()
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
|
||||||
|
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
|
||||||
|
|
||||||
|
// close issue by commit message
|
||||||
|
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
|
||||||
|
|
||||||
|
// call web hook
|
||||||
|
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||||
|
getWebHookURLs(repository.owner, repository.name) match {
|
||||||
|
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||||
|
for(ownerAccount <- getAccountByUserName(repository.owner)){
|
||||||
|
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||||
|
WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} getOrElse NotFound
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
|
||||||
|
@scala.annotation.tailrec
|
||||||
|
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
|
||||||
|
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
|
||||||
|
case true => _getPathObjectId(path, walk)
|
||||||
|
case false => None
|
||||||
|
}
|
||||||
|
|
||||||
|
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||||
|
treeWalk.addTree(revCommit.getTree)
|
||||||
|
treeWalk.setRecursive(true)
|
||||||
|
_getPathObjectId(path, treeWalk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
|
||||||
|
val revision = name.stripSuffix(suffix)
|
||||||
|
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
||||||
|
if(workDir.exists) {
|
||||||
|
FileUtils.deleteDirectory(workDir)
|
||||||
|
}
|
||||||
|
workDir.mkdirs
|
||||||
|
|
||||||
|
val file = new File(workDir, repository.name + "-" +
|
||||||
|
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
|
||||||
|
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||||
|
using(new java.io.FileOutputStream(file)) { out =>
|
||||||
|
git.archive
|
||||||
|
.setFormat(suffix.tail)
|
||||||
|
.setTree(revCommit.getTree)
|
||||||
|
.setOutputStream(out)
|
||||||
|
.call()
|
||||||
|
}
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
|
||||||
|
file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
50
src/main/scala/app/SearchController.scala
Normal file
50
src/main/scala/app/SearchController.scala
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import util._
|
||||||
|
import ControlUtil._
|
||||||
|
import Implicits._
|
||||||
|
import service._
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
|
class SearchController extends SearchControllerBase
|
||||||
|
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
|
||||||
|
|
||||||
|
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||||
|
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
|
||||||
|
|
||||||
|
val searchForm = mapping(
|
||||||
|
"query" -> trim(text(required)),
|
||||||
|
"owner" -> trim(text(required)),
|
||||||
|
"repository" -> trim(text(required))
|
||||||
|
)(SearchForm.apply)
|
||||||
|
|
||||||
|
case class SearchForm(query: String, owner: String, repository: String)
|
||||||
|
|
||||||
|
post("/search", searchForm){ form =>
|
||||||
|
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||||
|
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
|
||||||
|
val page = try {
|
||||||
|
val i = params.getOrElse("page", "1").toInt
|
||||||
|
if(i <= 0) 1 else i
|
||||||
|
} catch {
|
||||||
|
case e: NumberFormatException => 1
|
||||||
|
}
|
||||||
|
|
||||||
|
target.toLowerCase match {
|
||||||
|
case "issue" => search.html.issues(
|
||||||
|
searchIssues(repository.owner, repository.name, query),
|
||||||
|
countFiles(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
|
||||||
|
case _ => search.html.code(
|
||||||
|
searchFiles(repository.owner, repository.name, query),
|
||||||
|
countIssues(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import util.Directory._
|
|
||||||
import util.{UsersAuthenticator, OwnerAuthenticator}
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
|
|
||||||
class SettingsController extends SettingsControllerBase
|
|
||||||
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
|
|
||||||
|
|
||||||
trait SettingsControllerBase extends ControllerBase {
|
|
||||||
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator =>
|
|
||||||
|
|
||||||
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
|
||||||
|
|
||||||
val optionsForm = mapping(
|
|
||||||
"description" -> trim(label("Description" , optional(text()))),
|
|
||||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
|
||||||
"isPrivate" -> trim(label("Repository Type", boolean()))
|
|
||||||
)(OptionsForm.apply)
|
|
||||||
|
|
||||||
case class CollaboratorForm(userName: String)
|
|
||||||
|
|
||||||
val collaboratorForm = mapping(
|
|
||||||
"userName" -> trim(label("Username", text(required, collaborator)))
|
|
||||||
)(CollaboratorForm.apply)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect to the Options page.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/settings")(ownerOnly { repository =>
|
|
||||||
redirect("/%s/%s/settings/options".format(repository.owner, repository.name))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the Options page.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/settings/options")(ownerOnly {
|
|
||||||
settings.html.options(_)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the repository options.
|
|
||||||
*/
|
|
||||||
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
|
||||||
saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate)
|
|
||||||
redirect("%s/%s/settings/options".format(repository.owner, repository.name))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the Collaborators page.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
|
|
||||||
settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON API for collaborator completion.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
|
|
||||||
contentType = formats("json")
|
|
||||||
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the collaborator.
|
|
||||||
*/
|
|
||||||
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
|
|
||||||
addCollaborator(repository.owner, repository.name, form.userName)
|
|
||||||
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the collaborator.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
|
|
||||||
removeCollaborator(repository.owner, repository.name, params("name"))
|
|
||||||
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the delete repository page.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/settings/delete")(ownerOnly {
|
|
||||||
settings.html.delete(_)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the repository.
|
|
||||||
*/
|
|
||||||
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
|
|
||||||
deleteRepository(repository.owner, repository.name)
|
|
||||||
|
|
||||||
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
|
|
||||||
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
|
|
||||||
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
|
|
||||||
|
|
||||||
redirect("/%s".format(repository.owner))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides Constraint to validate the collaborator name.
|
|
||||||
*/
|
|
||||||
private def collaborator: Constraint = new Constraint(){
|
|
||||||
def validate(name: String, value: String): Option[String] = {
|
|
||||||
getAccountByUserName(value) match {
|
|
||||||
case None => Some("User does not exist.")
|
|
||||||
case Some(x) if(x.userName == context.loginAccount.get.userName) => Some("User can access this repository already.")
|
|
||||||
case Some(x) => {
|
|
||||||
val paths = request.getRequestURI.split("/")
|
|
||||||
if(getCollaborators(paths(1), paths(2)).contains(x.userName)){
|
|
||||||
Some("User can access this repository already.")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import util.StringUtil._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
|
|
||||||
|
|
||||||
trait SignInControllerBase extends ControllerBase { self: SystemSettingsService with AccountService =>
|
|
||||||
|
|
||||||
case class SignInForm(userName: String, password: String)
|
|
||||||
|
|
||||||
val form = mapping(
|
|
||||||
"userName" -> trim(label("Username", text(required))),
|
|
||||||
"password" -> trim(label("Password", text(required)))
|
|
||||||
)(SignInForm.apply)
|
|
||||||
|
|
||||||
get("/signin"){
|
|
||||||
val queryString = request.getQueryString
|
|
||||||
if(queryString != null && queryString.startsWith("/")){
|
|
||||||
session.setAttribute("REDIRECT", queryString)
|
|
||||||
}
|
|
||||||
html.signin(loadSystemSettings())
|
|
||||||
}
|
|
||||||
|
|
||||||
post("/signin", form){ form =>
|
|
||||||
val account = getAccountByUserName(form.userName)
|
|
||||||
if(account.isEmpty || account.get.password != encrypt(form.password)){
|
|
||||||
redirect("/signin")
|
|
||||||
} else {
|
|
||||||
session.setAttribute("LOGIN_ACCOUNT", account.get)
|
|
||||||
updateLastLoginDate(account.get.userName)
|
|
||||||
|
|
||||||
session.get("REDIRECT").map { redirectUrl =>
|
|
||||||
session.removeAttribute("REDIRECT")
|
|
||||||
redirect(redirectUrl.asInstanceOf[String])
|
|
||||||
}.getOrElse {
|
|
||||||
redirect("/%s".format(account.get.userName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get("/signout"){
|
|
||||||
session.invalidate
|
|
||||||
redirect("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -3,28 +3,183 @@ package app
|
|||||||
import service.{AccountService, SystemSettingsService}
|
import service.{AccountService, SystemSettingsService}
|
||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
import util.AdminAuthenticator
|
import util.AdminAuthenticator
|
||||||
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import ssh.SshServer
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import plugin.{Plugin, PluginSystem}
|
||||||
|
import org.scalatra.Ok
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
with SystemSettingsService with AccountService with AdminAuthenticator
|
with AccountService with AdminAuthenticator
|
||||||
|
|
||||||
trait SystemSettingsControllerBase extends ControllerBase {
|
trait SystemSettingsControllerBase extends ControllerBase {
|
||||||
self: SystemSettingsService with AccountService with AdminAuthenticator =>
|
self: AccountService with AdminAuthenticator =>
|
||||||
|
|
||||||
private case class SystemSettingsForm(allowAccountRegistration: Boolean)
|
|
||||||
|
|
||||||
private val form = mapping(
|
private val form = mapping(
|
||||||
"allowAccountRegistration" -> trim(label("Account registration", boolean()))
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
)(SystemSettingsForm.apply)
|
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||||
|
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||||
|
"notification" -> trim(label("Notification", boolean())),
|
||||||
|
"ssh" -> trim(label("SSH access", boolean())),
|
||||||
|
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||||
|
"smtp" -> optionalIfNotChecked("notification", mapping(
|
||||||
|
"host" -> trim(label("SMTP Host", text(required))),
|
||||||
|
"port" -> trim(label("SMTP Port", optional(number()))),
|
||||||
|
"user" -> trim(label("SMTP User", optional(text()))),
|
||||||
|
"password" -> trim(label("SMTP Password", optional(text()))),
|
||||||
|
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
|
||||||
|
"fromAddress" -> trim(label("FROM Address", optional(text()))),
|
||||||
|
"fromName" -> trim(label("FROM Name", optional(text())))
|
||||||
|
)(Smtp.apply)),
|
||||||
|
"ldapAuthentication" -> trim(label("LDAP", boolean())),
|
||||||
|
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
|
||||||
|
"host" -> trim(label("LDAP host", text(required))),
|
||||||
|
"port" -> trim(label("LDAP port", optional(number()))),
|
||||||
|
"bindDN" -> trim(label("Bind DN", optional(text()))),
|
||||||
|
"bindPassword" -> trim(label("Bind Password", optional(text()))),
|
||||||
|
"baseDN" -> trim(label("Base DN", text(required))),
|
||||||
|
"userNameAttribute" -> trim(label("User name attribute", text(required))),
|
||||||
|
"additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
|
||||||
|
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
|
||||||
|
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
|
||||||
|
"tls" -> trim(label("Enable TLS", optional(boolean()))),
|
||||||
|
"keystore" -> trim(label("Keystore", optional(text())))
|
||||||
|
)(Ldap.apply))
|
||||||
|
)(SystemSettings.apply).verifying { settings =>
|
||||||
|
if(settings.ssh && settings.baseUrl.isEmpty){
|
||||||
|
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
||||||
|
} else Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pluginForm = mapping(
|
||||||
|
"pluginId" -> list(trim(label("", text())))
|
||||||
|
)(PluginForm.apply)
|
||||||
|
|
||||||
|
case class PluginForm(pluginIds: List[String])
|
||||||
|
|
||||||
get("/admin/system")(adminOnly {
|
get("/admin/system")(adminOnly {
|
||||||
admin.html.system(loadSystemSettings())
|
admin.html.system(flash.get("info"))
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/system", form)(adminOnly { form =>
|
post("/admin/system", form)(adminOnly { form =>
|
||||||
saveSystemSettings(SystemSettings(form.allowAccountRegistration))
|
saveSystemSettings(form)
|
||||||
|
|
||||||
|
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
|
||||||
|
SshServer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
|
||||||
|
SshServer.start(request.getServletContext,
|
||||||
|
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
|
||||||
|
form.baseUrl.get)
|
||||||
|
} else if(!form.ssh && SshServer.isActive){
|
||||||
|
SshServer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
flash += "info" -> "System settings has been updated."
|
||||||
redirect("/admin/system")
|
redirect("/admin/system")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/admin/plugins")(adminOnly {
|
||||||
|
val installedPlugins = plugin.PluginSystem.plugins
|
||||||
|
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
|
||||||
|
admin.plugins.html.installed(installedPlugins, updatablePlugins)
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
|
||||||
|
deletePlugins(form.pluginIds)
|
||||||
|
installPlugins(form.pluginIds)
|
||||||
|
redirect("/admin/plugins")
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
|
||||||
|
deletePlugins(form.pluginIds)
|
||||||
|
redirect("/admin/plugins")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/plugins/available")(adminOnly {
|
||||||
|
val installedPlugins = plugin.PluginSystem.plugins
|
||||||
|
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
|
||||||
|
admin.plugins.html.available(availablePlugins)
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
|
||||||
|
installPlugins(form.pluginIds)
|
||||||
|
redirect("/admin/plugins")
|
||||||
|
})
|
||||||
|
|
||||||
|
// get("/admin/plugins/console")(adminOnly {
|
||||||
|
// admin.plugins.html.console()
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// post("/admin/plugins/console")(adminOnly {
|
||||||
|
// val script = request.getParameter("script")
|
||||||
|
// val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
|
||||||
|
// Ok(result)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// TODO Move these methods to PluginSystem or Service?
|
||||||
|
private def deletePlugins(pluginIds: List[String]): Unit = {
|
||||||
|
pluginIds.foreach { pluginId =>
|
||||||
|
plugin.PluginSystem.uninstall(pluginId)
|
||||||
|
val dir = new java.io.File(PluginHome, pluginId)
|
||||||
|
if(dir.exists && dir.isDirectory){
|
||||||
|
FileUtils.deleteQuietly(dir)
|
||||||
|
PluginSystem.uninstall(pluginId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def installPlugins(pluginIds: List[String]): Unit = {
|
||||||
|
val dir = getPluginCacheDir()
|
||||||
|
val installedPlugins = plugin.PluginSystem.plugins
|
||||||
|
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
|
||||||
|
val pluginDir = new java.io.File(PluginHome, plugin.id)
|
||||||
|
if(!pluginDir.exists){
|
||||||
|
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
|
||||||
|
}
|
||||||
|
PluginSystem.installPlugin(plugin.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
|
||||||
|
val repositoryRoot = getPluginCacheDir()
|
||||||
|
|
||||||
|
if(repositoryRoot.exists && repositoryRoot.isDirectory){
|
||||||
|
PluginSystem.repositories.flatMap { repo =>
|
||||||
|
val repoDir = new java.io.File(repositoryRoot, repo.id)
|
||||||
|
if(repoDir.exists && repoDir.isDirectory){
|
||||||
|
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
|
||||||
|
val propertyFile = new java.io.File(plugin, "plugin.properties")
|
||||||
|
val properties = new java.util.Properties()
|
||||||
|
if(propertyFile.exists && propertyFile.isFile){
|
||||||
|
using(new FileInputStream(propertyFile)){ in =>
|
||||||
|
properties.load(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemSettingsControllerBase.AvailablePlugin(
|
||||||
|
repository = repo.id,
|
||||||
|
id = properties.getProperty("id"),
|
||||||
|
version = properties.getProperty("version"),
|
||||||
|
author = properties.getProperty("author"),
|
||||||
|
url = properties.getProperty("url"),
|
||||||
|
description = properties.getProperty("description"),
|
||||||
|
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
|
||||||
|
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
|
||||||
|
case Some(x) => "installed"
|
||||||
|
case None => "available"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else Nil
|
||||||
|
}
|
||||||
|
} else Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SystemSettingsControllerBase {
|
||||||
|
case class AvailablePlugin(repository: String, id: String, version: String,
|
||||||
|
author: String, url: String, description: String, status: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,65 +3,191 @@ package app
|
|||||||
import service._
|
import service._
|
||||||
import util.AdminAuthenticator
|
import util.AdminAuthenticator
|
||||||
import util.StringUtil._
|
import util.StringUtil._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Directory._
|
||||||
|
import util.Implicits._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
|
||||||
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
|
class UserManagementController extends UserManagementControllerBase
|
||||||
|
with AccountService with RepositoryService with AdminAuthenticator
|
||||||
|
|
||||||
trait UserManagementControllerBase extends ControllerBase { self: AccountService with AdminAuthenticator =>
|
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||||
|
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||||
|
|
||||||
case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
|
case class NewUserForm(userName: String, password: String, fullName: String,
|
||||||
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean, url: Option[String])
|
mailAddress: String, isAdmin: Boolean,
|
||||||
|
url: Option[String], fileId: Option[String])
|
||||||
|
|
||||||
val newForm = mapping(
|
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, unique))),
|
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100)))),
|
|
||||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
members: String)
|
||||||
)(UserNewForm.apply)
|
|
||||||
|
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
|
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||||
|
|
||||||
|
val newUserForm = mapping(
|
||||||
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
|
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||||
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||||
|
)(NewUserForm.apply)
|
||||||
|
|
||||||
|
val editUserForm = mapping(
|
||||||
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||||
|
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||||
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||||
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean()))
|
||||||
|
)(EditUserForm.apply)
|
||||||
|
|
||||||
|
val newGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members)))
|
||||||
|
)(NewGroupForm.apply)
|
||||||
|
|
||||||
|
val editGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean()))
|
||||||
|
)(EditGroupForm.apply)
|
||||||
|
|
||||||
val editForm = mapping(
|
|
||||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
|
|
||||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100)))),
|
|
||||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
|
||||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
|
||||||
)(UserEditForm.apply)
|
|
||||||
|
|
||||||
get("/admin/users")(adminOnly {
|
get("/admin/users")(adminOnly {
|
||||||
admin.users.html.list(getAllUsers())
|
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||||
|
val users = getAllUsers(includeRemoved)
|
||||||
|
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||||
|
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
admin.users.html.list(users, members, includeRemoved)
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/users/_new")(adminOnly {
|
get("/admin/users/_newuser")(adminOnly {
|
||||||
admin.users.html.edit(None)
|
admin.users.html.user(None)
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/users/_new", newForm)(adminOnly { form =>
|
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||||
createAccount(form.userName, encrypt(form.password), form.mailAddress, form.isAdmin, form.url)
|
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
||||||
|
updateImage(form.userName, form.fileId, false)
|
||||||
redirect("/admin/users")
|
redirect("/admin/users")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/users/:userName/_edit")(adminOnly {
|
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
admin.users.html.edit(getAccountByUserName(userName))
|
admin.users.html.user(getAccountByUserName(userName, true))
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
|
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map { account =>
|
getAccountByUserName(userName, true).map { account =>
|
||||||
updateAccount(getAccountByUserName(userName).get.copy(
|
|
||||||
password = form.password.map(encrypt).getOrElse(account.password),
|
if(form.isRemoved){
|
||||||
|
// Remove repositories
|
||||||
|
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||||
|
deleteRepository(userName, repositoryName)
|
||||||
|
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||||
|
}
|
||||||
|
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||||
|
removeUserRelatedData(userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAccount(account.copy(
|
||||||
|
password = form.password.map(sha1).getOrElse(account.password),
|
||||||
|
fullName = form.fullName,
|
||||||
mailAddress = form.mailAddress,
|
mailAddress = form.mailAddress,
|
||||||
isAdmin = form.isAdmin,
|
isAdmin = form.isAdmin,
|
||||||
url = form.url))
|
url = form.url,
|
||||||
|
isRemoved = form.isRemoved))
|
||||||
|
|
||||||
|
updateImage(userName, form.fileId, form.clearImage)
|
||||||
redirect("/admin/users")
|
redirect("/admin/users")
|
||||||
|
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
private def unique: Constraint = new Constraint(){
|
get("/admin/users/_newgroup")(adminOnly {
|
||||||
def validate(name: String, value: String): Option[String] =
|
admin.users.html.group(None, Nil)
|
||||||
getAccountByUserName(value).map { _ => "User already exists." }
|
})
|
||||||
}
|
|
||||||
|
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||||
}
|
createGroup(form.groupName, form.url)
|
||||||
|
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList)
|
||||||
|
updateImage(form.groupName, form.fileId, false)
|
||||||
|
redirect("/admin/users")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||||
|
defining(params("groupName")){ groupName =>
|
||||||
|
admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||||
|
defining(params("groupName"), form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList){ case (groupName, members) =>
|
||||||
|
getAccountByUserName(groupName, true).map { account =>
|
||||||
|
updateGroup(groupName, form.url, form.isRemoved)
|
||||||
|
|
||||||
|
if(form.isRemoved){
|
||||||
|
// Remove from GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, Nil)
|
||||||
|
// Remove repositories
|
||||||
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
|
deleteRepository(groupName, repositoryName)
|
||||||
|
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, members)
|
||||||
|
// Update COLLABORATOR for group repositories
|
||||||
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
|
removeCollaborators(form.groupName, repositoryName)
|
||||||
|
members.foreach { case (userName, isManager) =>
|
||||||
|
addCollaborator(form.groupName, repositoryName, userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||||
|
redirect("/admin/users")
|
||||||
|
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private def members: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
if(value.split(",").exists {
|
||||||
|
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
||||||
|
}) None else Some("Must select one manager at least.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,82 +1,122 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil}
|
import util._
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Implicits._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
import java.util.ResourceBundle
|
||||||
|
|
||||||
class WikiController extends WikiControllerBase
|
class WikiController extends WikiControllerBase
|
||||||
with WikiService with RepositoryService with AccountService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||||
|
|
||||||
trait WikiControllerBase extends ControllerBase {
|
trait WikiControllerBase extends ControllerBase {
|
||||||
self: WikiService with RepositoryService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||||
|
|
||||||
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
|
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
|
||||||
|
|
||||||
val newForm = mapping(
|
val newForm = mapping(
|
||||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier, unique))),
|
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
|
||||||
"content" -> trim(label("Content" , text(required))),
|
"content" -> trim(label("Content" , text(required, conflictForNew))),
|
||||||
"message" -> trim(label("Message" , optional(text()))),
|
"message" -> trim(label("Message" , optional(text()))),
|
||||||
"currentPageName" -> trim(label("Current page name" , text()))
|
"currentPageName" -> trim(label("Current page name" , text())),
|
||||||
|
"id" -> trim(label("Latest commit id" , text()))
|
||||||
)(WikiPageEditForm.apply)
|
)(WikiPageEditForm.apply)
|
||||||
|
|
||||||
val editForm = mapping(
|
val editForm = mapping(
|
||||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier))),
|
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
|
||||||
"content" -> trim(label("Content" , text(required))),
|
"content" -> trim(label("Content" , text(required, conflictForEdit))),
|
||||||
"message" -> trim(label("Message" , optional(text()))),
|
"message" -> trim(label("Message" , optional(text()))),
|
||||||
"currentPageName" -> trim(label("Current page name" , text(required)))
|
"currentPageName" -> trim(label("Current page name" , text(required))),
|
||||||
|
"id" -> trim(label("Latest commit id" , text(required)))
|
||||||
)(WikiPageEditForm.apply)
|
)(WikiPageEditForm.apply)
|
||||||
|
|
||||||
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
||||||
getWikiPage(repository.owner, repository.name, "Home").map { page =>
|
getWikiPage(repository.owner, repository.name, "Home").map { page =>
|
||||||
wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
wiki.html.page("Home", page, getWikiPageList(repository.owner, repository.name),
|
||||||
} getOrElse redirect("/%s/%s/wiki/Home/_edit".format(repository.owner, repository.name))
|
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/:page")(referrersOnly { repository =>
|
||||||
val pageName = params("page")
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
|
|
||||||
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
||||||
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
wiki.html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
|
||||||
} getOrElse redirect("/%s/%s/wiki/%s/_edit".format(repository.owner, repository.name, pageName)) // TODO URLEncode
|
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
|
||||||
val pageName = params("page")
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
wiki.html.history(Some(pageName), JGitUtil.getCommitLog(git, "master", path = pageName + ".md")._1, repository)
|
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
|
||||||
|
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
|
||||||
|
case Left(_) => NotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
|
||||||
val pageName = params("page")
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
val commitId = params("commitId").split("\\.\\.\\.")
|
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), repository)
|
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
|
||||||
val commitId = params("commitId").split("\\.\\.\\.")
|
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), repository)
|
wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository =>
|
||||||
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
|
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||||
|
|
||||||
|
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
|
||||||
|
} else {
|
||||||
|
flash += "info" -> "This patch was not able to be reversed."
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository =>
|
||||||
|
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||||
|
|
||||||
|
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/")
|
||||||
|
} else {
|
||||||
|
flash += "info" -> "This patch was not able to be reversed."
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
|
||||||
val pageName = params("page")
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
||||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
defining(context.loginAccount.get){ loginAccount =>
|
||||||
form.content, context.loginAccount.get, form.message.getOrElse(""))
|
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
|
||||||
|
updateLastActivityDate(repository.owner, repository.name)
|
||||||
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
|
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
|
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
|
||||||
@@ -84,20 +124,26 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
defining(context.loginAccount.get){ loginAccount =>
|
||||||
form.content, context.loginAccount.get, form.message.getOrElse(""))
|
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
form.content, loginAccount, form.message.getOrElse(""), None)
|
||||||
|
|
||||||
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
|
updateLastActivityDate(repository.owner, repository.name)
|
||||||
|
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
|
||||||
val pageName = params("page")
|
val pageName = StringUtil.urlDecode(params("page"))
|
||||||
|
|
||||||
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, "Delete %s".format(pageName))
|
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
|
||||||
|
|
||||||
redirect("/%s/%s/wiki".format(repository.owner, repository.name))
|
defining(context.loginAccount.get){ loginAccount =>
|
||||||
|
deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}")
|
||||||
|
updateLastActivityDate(repository.owner, repository.name)
|
||||||
|
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
||||||
@@ -106,21 +152,55 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
wiki.html.history(None, JGitUtil.getCommitLog(git, "master")._1, repository)
|
JGitUtil.getCommitLog(git, "master") match {
|
||||||
|
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
|
||||||
|
case Left(_) => NotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
||||||
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
|
val path = multiParams("splat").head
|
||||||
contentType = "application/octet-stream"
|
|
||||||
content
|
getFileContent(repository.owner, repository.name, path).map { bytes =>
|
||||||
|
contentType = FileUtil.getContentType(path, bytes)
|
||||||
|
bytes
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
private def unique: Constraint = new Constraint(){
|
private def unique: Constraint = new Constraint(){
|
||||||
def validate(name: String, value: String): Option[String] =
|
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
|
||||||
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private def pagename: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
if(value.exists("\\/:*?\"<>|".contains(_))){
|
||||||
|
Some(s"${name} contains invalid character.")
|
||||||
|
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||||
|
Some(s"${name} starts with invalid character.")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def conflictForNew: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
targetWikiPage.map { _ =>
|
||||||
|
"Someone has created the wiki since you started. Please reload this page and re-apply your changes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def conflictForEdit: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
targetWikiPage.filter(_.id != params("id")).map{ _ =>
|
||||||
|
"Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait AccountComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
object Accounts extends Table[Account]("ACCOUNT") with Functions {
|
import self._
|
||||||
def userName = column[String]("USER_NAME", O PrimaryKey)
|
|
||||||
def mailAddress = column[String]("MAIL_ADDRESS")
|
lazy val Accounts = TableQuery[Accounts]
|
||||||
def password = column[String]("PASSWORD")
|
|
||||||
def isAdmin = column[Boolean]("ADMINISTRATOR")
|
class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") {
|
||||||
def url = column[String]("URL")
|
val userName = column[String]("USER_NAME", O PrimaryKey)
|
||||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
val fullName = column[String]("FULL_NAME")
|
||||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
val mailAddress = column[String]("MAIL_ADDRESS")
|
||||||
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
val password = column[String]("PASSWORD")
|
||||||
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? <> (Account, Account.unapply _)
|
val isAdmin = column[Boolean]("ADMINISTRATOR")
|
||||||
}
|
val url = column[String]("URL")
|
||||||
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
case class Account(
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
userName: String,
|
val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
||||||
mailAddress: String,
|
val image = column[String]("IMAGE")
|
||||||
password: String,
|
val groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||||
isAdmin: Boolean,
|
val removed = column[Boolean]("REMOVED")
|
||||||
url: Option[String],
|
def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply)
|
||||||
registeredDate: java.util.Date,
|
}
|
||||||
updatedDate: java.util.Date,
|
}
|
||||||
lastLoginDate: Option[java.util.Date]
|
|
||||||
)
|
case class Account(
|
||||||
|
userName: String,
|
||||||
|
fullName: String,
|
||||||
|
mailAddress: String,
|
||||||
|
password: String,
|
||||||
|
isAdmin: Boolean,
|
||||||
|
url: Option[String],
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
updatedDate: java.util.Date,
|
||||||
|
lastLoginDate: Option[java.util.Date],
|
||||||
|
image: Option[String],
|
||||||
|
isGroupAccount: Boolean,
|
||||||
|
isRemoved: Boolean
|
||||||
|
)
|
||||||
|
|||||||
29
src/main/scala/model/Activity.scala
Normal file
29
src/main/scala/model/Activity.scala
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait ActivityComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
|
lazy val Activities = TableQuery[Activities]
|
||||||
|
|
||||||
|
class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
|
||||||
|
val activityId = column[Int]("ACTIVITY_ID", O AutoInc)
|
||||||
|
val activityUserName = column[String]("ACTIVITY_USER_NAME")
|
||||||
|
val activityType = column[String]("ACTIVITY_TYPE")
|
||||||
|
val message = column[String]("MESSAGE")
|
||||||
|
val additionalInfo = column[String]("ADDITIONAL_INFO")
|
||||||
|
val activityDate = column[java.util.Date]("ACTIVITY_DATE")
|
||||||
|
def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Activity(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
activityUserName: String,
|
||||||
|
activityType: String,
|
||||||
|
message: String,
|
||||||
|
additionalInfo: Option[String],
|
||||||
|
activityDate: java.util.Date,
|
||||||
|
activityId: Int = 0
|
||||||
|
)
|
||||||
@@ -1,44 +1,47 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
protected[model] trait TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
protected[model] trait BasicTemplate { self: Table[_] =>
|
|
||||||
def userName = column[String]("USER_NAME")
|
trait BasicTemplate { self: Table[_] =>
|
||||||
def repositoryName = column[String]("REPOSITORY_NAME")
|
val userName = column[String]("USER_NAME")
|
||||||
|
val repositoryName = column[String]("REPOSITORY_NAME")
|
||||||
def byRepository(owner: String, repository: String) =
|
|
||||||
(userName is owner.bind) && (repositoryName is repository.bind)
|
def byRepository(owner: String, repository: String) =
|
||||||
|
(userName === owner.bind) && (repositoryName === repository.bind)
|
||||||
def byRepository(userName: Column[String], repositoryName: Column[String]) =
|
|
||||||
(this.userName is userName) && (this.repositoryName is repositoryName)
|
def byRepository(userName: Column[String], repositoryName: Column[String]) =
|
||||||
}
|
(this.userName === userName) && (this.repositoryName === repositoryName)
|
||||||
|
}
|
||||||
protected[model] trait IssueTemplate extends BasicTemplate { self: Table[_] =>
|
|
||||||
def issueId = column[Int]("ISSUE_ID")
|
trait IssueTemplate extends BasicTemplate { self: Table[_] =>
|
||||||
|
val issueId = column[Int]("ISSUE_ID")
|
||||||
def byIssue(owner: String, repository: String, issueId: Int) =
|
|
||||||
byRepository(owner, repository) && (this.issueId is issueId.bind)
|
def byIssue(owner: String, repository: String, issueId: Int) =
|
||||||
|
byRepository(owner, repository) && (this.issueId === issueId.bind)
|
||||||
def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
|
|
||||||
byRepository(userName, repositoryName) && (this.issueId is issueId)
|
def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
|
||||||
}
|
byRepository(userName, repositoryName) && (this.issueId === issueId)
|
||||||
|
}
|
||||||
protected[model] trait LabelTemplate extends BasicTemplate { self: Table[_] =>
|
|
||||||
def labelId = column[Int]("LABEL_ID")
|
trait LabelTemplate extends BasicTemplate { self: Table[_] =>
|
||||||
|
val labelId = column[Int]("LABEL_ID")
|
||||||
def byLabel(owner: String, repository: String, labelId: Int) =
|
|
||||||
byRepository(owner, repository) && (this.labelId is labelId.bind)
|
def byLabel(owner: String, repository: String, labelId: Int) =
|
||||||
|
byRepository(owner, repository) && (this.labelId === labelId.bind)
|
||||||
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
|
|
||||||
byRepository(userName, repositoryName) && (this.labelId is labelId)
|
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
|
||||||
}
|
byRepository(userName, repositoryName) && (this.labelId === labelId)
|
||||||
|
}
|
||||||
protected[model] trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
|
|
||||||
def milestoneId = column[Int]("MILESTONE_ID")
|
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
|
||||||
|
val milestoneId = column[Int]("MILESTONE_ID")
|
||||||
def byMilestone(owner: String, repository: String, milestoneId: Int) =
|
|
||||||
byRepository(owner, repository) && (this.milestoneId is milestoneId.bind)
|
def byMilestone(owner: String, repository: String, milestoneId: Int) =
|
||||||
|
byRepository(owner, repository) && (this.milestoneId === milestoneId.bind)
|
||||||
def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
|
|
||||||
byRepository(userName, repositoryName) && (this.milestoneId is milestoneId)
|
def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
|
||||||
}
|
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait CollaboratorComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
object Collaborators extends Table[Collaborator]("COLLABORATOR") with BasicTemplate {
|
lazy val Collaborators = TableQuery[Collaborators]
|
||||||
def collaboratorName = column[String]("COLLABORATOR_NAME")
|
|
||||||
def * = userName ~ repositoryName ~ collaboratorName <> (Collaborator, Collaborator.unapply _)
|
|
||||||
|
|
||||||
def byPrimaryKey(owner: String, repository: String, collaborator: String) =
|
class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate {
|
||||||
byRepository(owner, repository) && (collaboratorName is collaborator.bind)
|
val collaboratorName = column[String]("COLLABORATOR_NAME")
|
||||||
|
def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(owner: String, repository: String, collaborator: String) =
|
||||||
|
byRepository(owner, repository) && (collaboratorName === collaborator.bind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Collaborator(
|
case class Collaborator(
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import scala.slick.lifted.MappedTypeMapper
|
|
||||||
|
|
||||||
protected[model] trait Functions {
|
|
||||||
// java.util.Date TypeMapper
|
|
||||||
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
|
|
||||||
d => new java.sql.Timestamp(d.getTime),
|
|
||||||
t => new java.util.Date(t.getTime)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns system date.
|
|
||||||
*/
|
|
||||||
def currentDate = new java.util.Date()
|
|
||||||
|
|
||||||
}
|
|
||||||
20
src/main/scala/model/GroupMembers.scala
Normal file
20
src/main/scala/model/GroupMembers.scala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait GroupMemberComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
lazy val GroupMembers = TableQuery[GroupMembers]
|
||||||
|
|
||||||
|
class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") {
|
||||||
|
val groupName = column[String]("GROUP_NAME", O PrimaryKey)
|
||||||
|
val userName = column[String]("USER_NAME", O PrimaryKey)
|
||||||
|
val isManager = column[Boolean]("MANAGER")
|
||||||
|
def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GroupMember(
|
||||||
|
groupName: String,
|
||||||
|
userName: String,
|
||||||
|
isManager: Boolean
|
||||||
|
)
|
||||||
@@ -1,34 +1,49 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait IssueComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTemplate {
|
import self._
|
||||||
def * = userName ~ repositoryName ~ issueId
|
|
||||||
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
|
lazy val IssueId = TableQuery[IssueId]
|
||||||
}
|
lazy val IssueOutline = TableQuery[IssueOutline]
|
||||||
|
lazy val Issues = TableQuery[Issues]
|
||||||
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate with Functions {
|
|
||||||
def openedUserName = column[String]("OPENED_USER_NAME")
|
class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate {
|
||||||
def assignedUserName = column[String]("ASSIGNED_USER_NAME")
|
def * = (userName, repositoryName, issueId)
|
||||||
def title = column[String]("TITLE")
|
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
|
||||||
def content = column[String]("CONTENT")
|
}
|
||||||
def closed = column[Boolean]("CLOSED")
|
|
||||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate {
|
||||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
val commentCount = column[Int]("COMMENT_COUNT")
|
||||||
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate <> (Issue, Issue.unapply _)
|
def * = (userName, repositoryName, issueId, commentCount)
|
||||||
|
}
|
||||||
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
|
|
||||||
}
|
class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate {
|
||||||
|
val openedUserName = column[String]("OPENED_USER_NAME")
|
||||||
case class Issue(
|
val assignedUserName = column[String]("ASSIGNED_USER_NAME")
|
||||||
userName: String,
|
val title = column[String]("TITLE")
|
||||||
repositoryName: String,
|
val content = column[String]("CONTENT")
|
||||||
issueId: Int,
|
val closed = column[Boolean]("CLOSED")
|
||||||
openedUserName: String,
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
milestoneId: Option[Int],
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
assignedUserName: Option[String],
|
val pullRequest = column[Boolean]("PULL_REQUEST")
|
||||||
title: String,
|
def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply)
|
||||||
content: Option[String],
|
|
||||||
closed: Boolean,
|
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
|
||||||
registeredDate: java.util.Date,
|
}
|
||||||
updatedDate: java.util.Date)
|
}
|
||||||
|
|
||||||
|
case class Issue(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
issueId: Int,
|
||||||
|
openedUserName: String,
|
||||||
|
milestoneId: Option[Int],
|
||||||
|
assignedUserName: Option[String],
|
||||||
|
title: String,
|
||||||
|
content: Option[String],
|
||||||
|
closed: Boolean,
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
updatedDate: java.util.Date,
|
||||||
|
isPullRequest: Boolean
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate with Functions {
|
import self._
|
||||||
def commentId = column[Int]("COMMENT_ID", O AutoInc)
|
|
||||||
def action = column[String]("ACTION")
|
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
|
||||||
def commentedUserName = column[String]("COMMENTED_USER_NAME")
|
def autoInc = this returning this.map(_.commentId)
|
||||||
def content = column[String]("CONTENT")
|
}
|
||||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
|
||||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
|
||||||
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
|
val commentId = column[Int]("COMMENT_ID", O AutoInc)
|
||||||
|
val action = column[String]("ACTION")
|
||||||
def autoInc = userName ~ repositoryName ~ issueId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
|
val commentedUserName = column[String]("COMMENTED_USER_NAME")
|
||||||
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
|
val content = column[String]("CONTENT")
|
||||||
}
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
case class IssueComment(
|
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
|
||||||
userName: String,
|
|
||||||
repositoryName: String,
|
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
||||||
issueId: Int,
|
}
|
||||||
commentId: Int,
|
}
|
||||||
action: Option[String],
|
|
||||||
commentedUserName: String,
|
case class IssueComment(
|
||||||
content: String,
|
userName: String,
|
||||||
registeredDate: java.util.Date,
|
repositoryName: String,
|
||||||
updatedDate: java.util.Date
|
issueId: Int,
|
||||||
)
|
commentId: Int = 0,
|
||||||
|
action: String,
|
||||||
|
commentedUserName: String,
|
||||||
|
content: String,
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
updatedDate: java.util.Date
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait IssueLabelComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
object IssueLabels extends Table[IssueLabel]("ISSUE_LABEL") with IssueTemplate with LabelTemplate {
|
lazy val IssueLabels = TableQuery[IssueLabels]
|
||||||
def * = userName ~ repositoryName ~ issueId ~ labelId <> (IssueLabel, IssueLabel.unapply _)
|
|
||||||
def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
|
class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate {
|
||||||
byIssue(owner, repository, issueId) && (this.labelId is labelId.bind)
|
def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply)
|
||||||
|
def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
|
||||||
|
byIssue(owner, repository, issueId) && (this.labelId === labelId.bind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case class IssueLabel(
|
case class IssueLabel(
|
||||||
userName: String,
|
userName: String,
|
||||||
repositoryName: String,
|
repositoryName: String,
|
||||||
issueId: Int,
|
issueId: Int,
|
||||||
labelId: Int)
|
labelId: Int
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait LabelComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
object Labels extends Table[Label]("LABEL") with LabelTemplate {
|
lazy val Labels = TableQuery[Labels]
|
||||||
def labelName = column[String]("LABEL_NAME")
|
|
||||||
def color = column[String]("COLOR")
|
|
||||||
def * = userName ~ repositoryName ~ labelId ~ labelName ~ color <> (Label, Label.unapply _)
|
|
||||||
|
|
||||||
def ins = userName ~ repositoryName ~ labelName ~ color
|
class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate {
|
||||||
def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
|
override val labelId = column[Int]("LABEL_ID", O AutoInc)
|
||||||
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
|
val labelName = column[String]("LABEL_NAME")
|
||||||
|
val color = column[String]("COLOR")
|
||||||
|
def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
|
||||||
|
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Label(
|
case class Label(
|
||||||
userName: String,
|
userName: String,
|
||||||
repositoryName: String,
|
repositoryName: String,
|
||||||
labelId: Int,
|
labelId: Int = 0,
|
||||||
labelName: String,
|
labelName: String,
|
||||||
color: String){
|
color: String){
|
||||||
|
|
||||||
@@ -30,5 +34,4 @@ case class Label(
|
|||||||
"FFFFFF"
|
"FFFFFF"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait MilestoneComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate with Functions {
|
lazy val Milestones = TableQuery[Milestones]
|
||||||
def title = column[String]("TITLE")
|
|
||||||
def description = column[String]("DESCRIPTION")
|
|
||||||
def dueDate = column[java.util.Date]("DUE_DATE")
|
|
||||||
def closedDate = column[java.util.Date]("CLOSED_DATE")
|
|
||||||
def * = userName ~ repositoryName ~ milestoneId ~ title ~ description.? ~ dueDate.? ~ closedDate.? <> (Milestone, Milestone.unapply _)
|
|
||||||
|
|
||||||
def ins = userName ~ repositoryName ~ title ~ description.? ~ dueDate.? ~ closedDate.?
|
class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate {
|
||||||
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
|
override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc)
|
||||||
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
|
val title = column[String]("TITLE")
|
||||||
|
val description = column[String]("DESCRIPTION")
|
||||||
|
val dueDate = column[java.util.Date]("DUE_DATE")
|
||||||
|
val closedDate = column[java.util.Date]("CLOSED_DATE")
|
||||||
|
def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
|
||||||
|
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Milestone(
|
case class Milestone(
|
||||||
userName: String,
|
userName: String,
|
||||||
repositoryName: String,
|
repositoryName: String,
|
||||||
milestoneId: Int,
|
milestoneId: Int = 0,
|
||||||
title: String,
|
title: String,
|
||||||
description: Option[String],
|
description: Option[String],
|
||||||
dueDate: Option[java.util.Date],
|
dueDate: Option[java.util.Date],
|
||||||
closedDate: Option[java.util.Date])
|
closedDate: Option[java.util.Date]
|
||||||
|
)
|
||||||
|
|||||||
41
src/main/scala/model/Profile.scala
Normal file
41
src/main/scala/model/Profile.scala
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait Profile {
|
||||||
|
val profile: slick.driver.JdbcProfile
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
// java.util.Date Mapped Column Types
|
||||||
|
implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp](
|
||||||
|
d => new java.sql.Timestamp(d.getTime),
|
||||||
|
t => new java.util.Date(t.getTime)
|
||||||
|
)
|
||||||
|
|
||||||
|
implicit class RichColumn(c1: Column[Boolean]){
|
||||||
|
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object Profile extends {
|
||||||
|
val profile = slick.driver.H2Driver
|
||||||
|
|
||||||
|
} with AccountComponent
|
||||||
|
with ActivityComponent
|
||||||
|
with CollaboratorComponent
|
||||||
|
with GroupMemberComponent
|
||||||
|
with IssueComponent
|
||||||
|
with IssueCommentComponent
|
||||||
|
with IssueLabelComponent
|
||||||
|
with LabelComponent
|
||||||
|
with MilestoneComponent
|
||||||
|
with PullRequestComponent
|
||||||
|
with RepositoryComponent
|
||||||
|
with SshKeyComponent
|
||||||
|
with WebHookComponent with Profile {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns system date.
|
||||||
|
*/
|
||||||
|
def currentDate = new java.util.Date()
|
||||||
|
|
||||||
|
}
|
||||||
32
src/main/scala/model/PullRequest.scala
Normal file
32
src/main/scala/model/PullRequest.scala
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait PullRequestComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
lazy val PullRequests = TableQuery[PullRequests]
|
||||||
|
|
||||||
|
class PullRequests(tag: Tag) extends Table[PullRequest](tag, "PULL_REQUEST") with IssueTemplate {
|
||||||
|
val branch = column[String]("BRANCH")
|
||||||
|
val requestUserName = column[String]("REQUEST_USER_NAME")
|
||||||
|
val requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
|
||||||
|
val requestBranch = column[String]("REQUEST_BRANCH")
|
||||||
|
val commitIdFrom = column[String]("COMMIT_ID_FROM")
|
||||||
|
val commitIdTo = column[String]("COMMIT_ID_TO")
|
||||||
|
def * = (userName, repositoryName, issueId, branch, requestUserName, requestRepositoryName, requestBranch, commitIdFrom, commitIdTo) <> (PullRequest.tupled, PullRequest.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
|
||||||
|
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class PullRequest(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
issueId: Int,
|
||||||
|
branch: String,
|
||||||
|
requestUserName: String,
|
||||||
|
requestRepositoryName: String,
|
||||||
|
requestBranch: String,
|
||||||
|
commitIdFrom: String,
|
||||||
|
commitIdTo: String
|
||||||
|
)
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
trait RepositoryComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate with Functions {
|
lazy val Repositories = TableQuery[Repositories]
|
||||||
def isPrivate = column[Boolean]("PRIVATE")
|
|
||||||
def description = column[String]("DESCRIPTION")
|
|
||||||
def defaultBranch = column[String]("DEFAULT_BRANCH")
|
|
||||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
|
||||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
|
||||||
def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
|
|
||||||
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate <> (Repository, Repository.unapply _)
|
|
||||||
|
|
||||||
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
|
class Repositories(tag: Tag) extends Table[Repository](tag, "REPOSITORY") with BasicTemplate {
|
||||||
|
val isPrivate = column[Boolean]("PRIVATE")
|
||||||
|
val description = column[String]("DESCRIPTION")
|
||||||
|
val defaultBranch = column[String]("DEFAULT_BRANCH")
|
||||||
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
|
val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
|
||||||
|
val originUserName = column[String]("ORIGIN_USER_NAME")
|
||||||
|
val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
|
||||||
|
val parentUserName = column[String]("PARENT_USER_NAME")
|
||||||
|
val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
|
||||||
|
def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?) <> (Repository.tupled, Repository.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Repository(
|
case class Repository(
|
||||||
@@ -22,5 +31,9 @@ case class Repository(
|
|||||||
defaultBranch: String,
|
defaultBranch: String,
|
||||||
registeredDate: java.util.Date,
|
registeredDate: java.util.Date,
|
||||||
updatedDate: java.util.Date,
|
updatedDate: java.util.Date,
|
||||||
lastActivityDate: java.util.Date
|
lastActivityDate: java.util.Date,
|
||||||
|
originUserName: Option[String],
|
||||||
|
originRepositoryName: Option[String],
|
||||||
|
parentUserName: Option[String],
|
||||||
|
parentRepositoryName: Option[String]
|
||||||
)
|
)
|
||||||
|
|||||||
24
src/main/scala/model/SshKey.scala
Normal file
24
src/main/scala/model/SshKey.scala
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait SshKeyComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
lazy val SshKeys = TableQuery[SshKeys]
|
||||||
|
|
||||||
|
class SshKeys(tag: Tag) extends Table[SshKey](tag, "SSH_KEY") {
|
||||||
|
val userName = column[String]("USER_NAME")
|
||||||
|
val sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc)
|
||||||
|
val title = column[String]("TITLE")
|
||||||
|
val publicKey = column[String]("PUBLIC_KEY")
|
||||||
|
def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class SshKey(
|
||||||
|
userName: String,
|
||||||
|
sshKeyId: Int = 0,
|
||||||
|
title: String,
|
||||||
|
publicKey: String
|
||||||
|
)
|
||||||
20
src/main/scala/model/WebHook.scala
Normal file
20
src/main/scala/model/WebHook.scala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait WebHookComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
lazy val WebHooks = TableQuery[WebHooks]
|
||||||
|
|
||||||
|
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||||
|
val url = column[String]("URL")
|
||||||
|
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class WebHook(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
url: String
|
||||||
|
)
|
||||||
3
src/main/scala/model/package.scala
Normal file
3
src/main/scala/model/package.scala
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package object model {
|
||||||
|
type Session = slick.jdbc.JdbcBackend#Session
|
||||||
|
}
|
||||||
117
src/main/scala/plugin/JavaScriptPlugin.scala
Normal file
117
src/main/scala/plugin/JavaScriptPlugin.scala
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import org.mozilla.javascript.{Context => JsContext}
|
||||||
|
import org.mozilla.javascript.{Function => JsFunction}
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import plugin.PluginSystem._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import plugin.PluginSystem.GlobalMenu
|
||||||
|
import plugin.PluginSystem.RepositoryAction
|
||||||
|
import plugin.PluginSystem.Action
|
||||||
|
import plugin.PluginSystem.RepositoryMenu
|
||||||
|
|
||||||
|
class JavaScriptPlugin(val id: String, val version: String,
|
||||||
|
val author: String, val url: String, val description: String) extends Plugin {
|
||||||
|
|
||||||
|
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
|
||||||
|
private val globalMenuList = ListBuffer[GlobalMenu]()
|
||||||
|
private val repositoryActionList = ListBuffer[RepositoryAction]()
|
||||||
|
private val globalActionList = ListBuffer[Action]()
|
||||||
|
|
||||||
|
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
|
||||||
|
def globalMenus : List[GlobalMenu] = globalMenuList.toList
|
||||||
|
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
|
||||||
|
def globalActions : List[Action] = globalActionList.toList
|
||||||
|
|
||||||
|
def addRepositoryMenu(label: String, name: String, url: String, icon: String, condition: JsFunction): Unit = {
|
||||||
|
repositoryMenuList += RepositoryMenu(label, name, url, icon, (context) => {
|
||||||
|
val context = JsContext.enter()
|
||||||
|
try {
|
||||||
|
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
|
||||||
|
} finally {
|
||||||
|
JsContext.exit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def addGlobalMenu(label: String, url: String, icon: String, condition: JsFunction): Unit = {
|
||||||
|
globalMenuList += GlobalMenu(label, url, icon, (context) => {
|
||||||
|
val context = JsContext.enter()
|
||||||
|
try {
|
||||||
|
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
|
||||||
|
} finally {
|
||||||
|
JsContext.exit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def addGlobalAction(path: String, function: JsFunction): Unit = {
|
||||||
|
globalActionList += Action(path, (request, response) => {
|
||||||
|
val context = JsContext.enter()
|
||||||
|
try {
|
||||||
|
function.call(context, function, function, Array(request, response))
|
||||||
|
} finally {
|
||||||
|
JsContext.exit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def addRepositoryAction(path: String, function: JsFunction): Unit = {
|
||||||
|
repositoryActionList += RepositoryAction(path, (request, response, repository) => {
|
||||||
|
val context = JsContext.enter()
|
||||||
|
try {
|
||||||
|
function.call(context, function, function, Array(request, response, repository))
|
||||||
|
} finally {
|
||||||
|
JsContext.exit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
object db {
|
||||||
|
// TODO Use JavaScript Map instead of java.util.Map
|
||||||
|
def select(sql: String): Array[java.util.Map[String, String]] = {
|
||||||
|
defining(PluginConnectionHolder.threadLocal.get){ conn =>
|
||||||
|
using(conn.prepareStatement(sql)){ stmt =>
|
||||||
|
using(stmt.executeQuery()){ rs =>
|
||||||
|
val list = new java.util.ArrayList[java.util.Map[String, String]]()
|
||||||
|
while(rs.next){
|
||||||
|
defining(rs.getMetaData){ meta =>
|
||||||
|
val map = new java.util.HashMap[String, String]()
|
||||||
|
Range(1, meta.getColumnCount).map { i =>
|
||||||
|
val name = meta.getColumnName(i)
|
||||||
|
map.put(name, rs.getString(name))
|
||||||
|
}
|
||||||
|
list.add(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.toArray(new Array[java.util.Map[String, String]](list.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object JavaScriptPlugin {
|
||||||
|
|
||||||
|
def define(id: String, version: String, author: String, url: String, description: String)
|
||||||
|
= new JavaScriptPlugin(id, version, author, url, description)
|
||||||
|
|
||||||
|
def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = {
|
||||||
|
val context = JsContext.enter()
|
||||||
|
try {
|
||||||
|
val scope = context.initStandardObjects()
|
||||||
|
scope.put("PluginSystem", scope, PluginSystem)
|
||||||
|
scope.put("JavaScriptPlugin", scope, this)
|
||||||
|
vars.foreach { case (key, value) =>
|
||||||
|
scope.put(key, scope, value)
|
||||||
|
}
|
||||||
|
val result = context.evaluateString(scope, script, "<cmd>", 1, null)
|
||||||
|
result
|
||||||
|
} finally {
|
||||||
|
JsContext.exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/main/scala/plugin/Plugin.scala
Normal file
21
src/main/scala/plugin/Plugin.scala
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import plugin.PluginSystem._
|
||||||
|
import java.sql.Connection
|
||||||
|
|
||||||
|
trait Plugin {
|
||||||
|
val id: String
|
||||||
|
val version: String
|
||||||
|
val author: String
|
||||||
|
val url: String
|
||||||
|
val description: String
|
||||||
|
|
||||||
|
def repositoryMenus : List[RepositoryMenu]
|
||||||
|
def globalMenus : List[GlobalMenu]
|
||||||
|
def repositoryActions : List[RepositoryAction]
|
||||||
|
def globalActions : List[Action]
|
||||||
|
}
|
||||||
|
|
||||||
|
object PluginConnectionHolder {
|
||||||
|
val threadLocal = new ThreadLocal[Connection]
|
||||||
|
}
|
||||||
125
src/main/scala/plugin/PluginSystem.scala
Normal file
125
src/main/scala/plugin/PluginSystem.scala
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import app.Context
|
||||||
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import util.JGitUtil
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import service.RepositoryService.RepositoryInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides extension points to plug-ins.
|
||||||
|
*/
|
||||||
|
object PluginSystem {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
|
||||||
|
|
||||||
|
private val initialized = new AtomicBoolean(false)
|
||||||
|
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
|
||||||
|
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
|
||||||
|
|
||||||
|
def install(plugin: Plugin): Unit = {
|
||||||
|
pluginsMap.put(plugin.id, plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
def plugins: List[Plugin] = pluginsMap.values.toList
|
||||||
|
|
||||||
|
def uninstall(id: String): Unit = {
|
||||||
|
pluginsMap.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
def repositories: List[PluginRepository] = repositoriesList.toList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
|
||||||
|
*/
|
||||||
|
def init(): Unit = {
|
||||||
|
if(initialized.compareAndSet(false, true)){
|
||||||
|
// Load installed plugins
|
||||||
|
val pluginDir = new java.io.File(PluginHome)
|
||||||
|
if(pluginDir.exists && pluginDir.isDirectory){
|
||||||
|
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
|
||||||
|
installPlugin(dir.getName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add default plugin repositories
|
||||||
|
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Method name seems to not so good.
|
||||||
|
def installPlugin(id: String): Unit = {
|
||||||
|
val pluginDir = new java.io.File(PluginHome)
|
||||||
|
val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js")
|
||||||
|
|
||||||
|
if(javaScriptFile.exists && javaScriptFile.isFile){
|
||||||
|
val properties = new java.util.Properties()
|
||||||
|
using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in =>
|
||||||
|
properties.load(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
val script = FileUtils.readFileToString(javaScriptFile, "UTF-8")
|
||||||
|
try {
|
||||||
|
JavaScriptPlugin.evaluateJavaScript(script, Map(
|
||||||
|
"id" -> properties.getProperty("id"),
|
||||||
|
"version" -> properties.getProperty("version"),
|
||||||
|
"author" -> properties.getProperty("author"),
|
||||||
|
"url" -> properties.getProperty("url"),
|
||||||
|
"description" -> properties.getProperty("description")
|
||||||
|
))
|
||||||
|
} catch {
|
||||||
|
case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
|
||||||
|
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
|
||||||
|
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
|
||||||
|
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
|
||||||
|
|
||||||
|
// Case classes to hold plug-ins information internally in GitBucket
|
||||||
|
case class PluginRepository(id: String, url: String)
|
||||||
|
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
|
||||||
|
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
|
||||||
|
case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any)
|
||||||
|
case class RepositoryAction(path: String, function: (HttpServletRequest, HttpServletResponse, RepositoryInfo) => Any)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the plugin is updatable.
|
||||||
|
*/
|
||||||
|
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
|
||||||
|
if(oldVersion == newVersion){
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val dim1 = oldVersion.split("\\.").map(_.toInt)
|
||||||
|
val dim2 = newVersion.split("\\.").map(_.toInt)
|
||||||
|
dim1.zip(dim2).foreach { case (a, b) =>
|
||||||
|
if(a < b){
|
||||||
|
return true
|
||||||
|
} else if(a > b){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO This is a test
|
||||||
|
// addGlobalMenu("Google", "http://www.google.co.jp/", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
|
||||||
|
// { context => context.loginAccount.isDefined }
|
||||||
|
//
|
||||||
|
// addRepositoryMenu("Board", "board", "/board", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
|
||||||
|
// { context => true}
|
||||||
|
//
|
||||||
|
// addGlobalAction("/hello"){ (request, response) =>
|
||||||
|
// "Hello World!"
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
66
src/main/scala/plugin/PluginUpdateJob.scala
Normal file
66
src/main/scala/plugin/PluginUpdateJob.scala
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import util.Directory._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.quartz.{Scheduler, JobExecutionContext, Job}
|
||||||
|
import org.quartz.JobBuilder._
|
||||||
|
import org.quartz.TriggerBuilder._
|
||||||
|
import org.quartz.SimpleScheduleBuilder._
|
||||||
|
|
||||||
|
class PluginUpdateJob extends Job {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
|
||||||
|
private var failedCount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone or pull all plugin repositories
|
||||||
|
*
|
||||||
|
* TODO Support plugin repository access through the proxy server
|
||||||
|
*/
|
||||||
|
override def execute(context: JobExecutionContext): Unit = {
|
||||||
|
try {
|
||||||
|
if(failedCount > 3){
|
||||||
|
logger.error("Skip plugin information updating because failed count is over limit")
|
||||||
|
} else {
|
||||||
|
logger.info("Start plugin information updating")
|
||||||
|
PluginSystem.repositories.foreach { repository =>
|
||||||
|
logger.info(s"Updating ${repository.id}: ${repository.url}...")
|
||||||
|
val dir = getPluginCacheDir()
|
||||||
|
val repo = new java.io.File(dir, repository.id)
|
||||||
|
if(repo.exists){
|
||||||
|
// pull if the repository is already cloned
|
||||||
|
Git.open(repo).pull().call()
|
||||||
|
} else {
|
||||||
|
// clone if the repository is not exist
|
||||||
|
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("End plugin information updating")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case e: Exception => {
|
||||||
|
failedCount = failedCount + 1
|
||||||
|
logger.error("Failed to update plugin information", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PluginUpdateJob {
|
||||||
|
|
||||||
|
def schedule(scheduler: Scheduler): Unit = {
|
||||||
|
val job = newJob(classOf[PluginUpdateJob])
|
||||||
|
.withIdentity("pluginUpdateJob")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val trigger = newTrigger()
|
||||||
|
.withIdentity("pluginUpdateTrigger")
|
||||||
|
.startNow()
|
||||||
|
.withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
scheduler.scheduleJob(job, trigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
src/main/scala/plugin/ScalaPlugin.scala
Normal file
39
src/main/scala/plugin/ScalaPlugin.scala
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import app.Context
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import plugin.PluginSystem._
|
||||||
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
|
import service.RepositoryService.RepositoryInfo
|
||||||
|
|
||||||
|
// TODO This is a sample implementation for Scala based plug-ins.
|
||||||
|
class ScalaPlugin(val id: String, val version: String,
|
||||||
|
val author: String, val url: String, val description: String) extends Plugin {
|
||||||
|
|
||||||
|
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
|
||||||
|
private val globalMenuList = ListBuffer[GlobalMenu]()
|
||||||
|
private val repositoryActionList = ListBuffer[RepositoryAction]()
|
||||||
|
private val globalActionList = ListBuffer[Action]()
|
||||||
|
|
||||||
|
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
|
||||||
|
def globalMenus : List[GlobalMenu] = globalMenuList.toList
|
||||||
|
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
|
||||||
|
def globalActions : List[Action] = globalActionList.toList
|
||||||
|
|
||||||
|
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
|
||||||
|
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
|
||||||
|
globalMenuList += GlobalMenu(label, url, icon, condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
def addGlobalAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
|
||||||
|
globalActionList += Action(path, function)
|
||||||
|
}
|
||||||
|
|
||||||
|
def addRepositoryAction(path: String)(function: (HttpServletRequest, HttpServletResponse, RepositoryInfo) => Any): Unit = {
|
||||||
|
repositoryActionList += RepositoryAction(path, function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,42 +1,173 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import model._
|
import model.Profile._
|
||||||
import Accounts._
|
import profile.simple._
|
||||||
import scala.slick.driver.H2Driver.simple._
|
import model.{Account, GroupMember}
|
||||||
import Database.threadLocalSession
|
// TODO [Slick 2.0]NOT import directly?
|
||||||
|
import model.Profile.dateColumnType
|
||||||
|
import service.SystemSettingsService.SystemSettings
|
||||||
|
import util.StringUtil._
|
||||||
|
import util.LDAPUtil
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
trait AccountService {
|
trait AccountService {
|
||||||
|
|
||||||
def getAccountByUserName(userName: String): Option[Account] =
|
private val logger = LoggerFactory.getLogger(classOf[AccountService])
|
||||||
Query(Accounts) filter(_.userName is userName.bind) firstOption
|
|
||||||
|
def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] =
|
||||||
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
|
if(settings.ldapAuthentication){
|
||||||
|
ldapAuthentication(settings, userName, password)
|
||||||
def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
|
} else {
|
||||||
|
defaultAuthentication(userName, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate by internal database.
|
||||||
|
*/
|
||||||
|
private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = {
|
||||||
|
getAccountByUserName(userName).collect {
|
||||||
|
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
|
||||||
|
} getOrElse None
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate by LDAP.
|
||||||
|
*/
|
||||||
|
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String)
|
||||||
|
(implicit s: Session): Option[Account] = {
|
||||||
|
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
|
||||||
|
case Right(ldapUserInfo) => {
|
||||||
|
// Create or update account by LDAP information
|
||||||
|
getAccountByUserName(ldapUserInfo.userName, true) match {
|
||||||
|
case Some(x) if(!x.isRemoved) => {
|
||||||
|
if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
|
||||||
|
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||||
|
} else {
|
||||||
|
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
|
||||||
|
}
|
||||||
|
getAccountByUserName(ldapUserInfo.userName)
|
||||||
|
}
|
||||||
|
case Some(x) if(x.isRemoved) => {
|
||||||
|
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||||
|
defaultAuthentication(userName, password)
|
||||||
|
}
|
||||||
|
case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
|
||||||
|
case Some(x) if(!x.isRemoved) => {
|
||||||
|
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||||
|
getAccountByUserName(ldapUserInfo.userName)
|
||||||
|
}
|
||||||
|
case Some(x) if(x.isRemoved) => {
|
||||||
|
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||||
|
defaultAuthentication(userName, password)
|
||||||
|
}
|
||||||
|
case None => {
|
||||||
|
createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
|
||||||
|
getAccountByUserName(ldapUserInfo.userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Left(errorMessage) => {
|
||||||
|
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
|
||||||
|
defaultAuthentication(userName, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
|
||||||
|
Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
|
||||||
|
|
||||||
|
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
|
||||||
|
Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
|
||||||
|
|
||||||
|
def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] =
|
||||||
|
if(includeRemoved){
|
||||||
|
Accounts sortBy(_.userName) list
|
||||||
|
} else {
|
||||||
|
Accounts filter (_.removed === false.bind) sortBy(_.userName) list
|
||||||
|
}
|
||||||
|
|
||||||
|
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
|
||||||
|
(implicit s: Session): Unit =
|
||||||
Accounts insert Account(
|
Accounts insert Account(
|
||||||
userName = userName,
|
userName = userName,
|
||||||
password = password,
|
password = password,
|
||||||
|
fullName = fullName,
|
||||||
mailAddress = mailAddress,
|
mailAddress = mailAddress,
|
||||||
isAdmin = isAdmin,
|
isAdmin = isAdmin,
|
||||||
url = url,
|
url = url,
|
||||||
registeredDate = currentDate,
|
registeredDate = currentDate,
|
||||||
updatedDate = currentDate,
|
updatedDate = currentDate,
|
||||||
lastLoginDate = None)
|
lastLoginDate = None,
|
||||||
|
image = None,
|
||||||
|
isGroupAccount = false,
|
||||||
|
isRemoved = false)
|
||||||
|
|
||||||
def updateAccount(account: Account): Unit =
|
def updateAccount(account: Account)(implicit s: Session): Unit =
|
||||||
Accounts
|
Accounts
|
||||||
.filter { a => a.userName is account.userName.bind }
|
.filter { a => a.userName === account.userName.bind }
|
||||||
.map { a => a.password ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? }
|
.map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) }
|
||||||
.update (
|
.update (
|
||||||
account.password,
|
account.password,
|
||||||
account.mailAddress,
|
account.fullName,
|
||||||
|
account.mailAddress,
|
||||||
account.isAdmin,
|
account.isAdmin,
|
||||||
account.url,
|
account.url,
|
||||||
account.registeredDate,
|
account.registeredDate,
|
||||||
currentDate,
|
currentDate,
|
||||||
account.lastLoginDate)
|
account.lastLoginDate,
|
||||||
|
account.isRemoved)
|
||||||
def updateLastLoginDate(userName: String): Unit =
|
|
||||||
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
|
def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit =
|
||||||
|
Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image)
|
||||||
|
|
||||||
|
def updateLastLoginDate(userName: String)(implicit s: Session): Unit =
|
||||||
|
Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate)
|
||||||
|
|
||||||
|
def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit =
|
||||||
|
Accounts insert Account(
|
||||||
|
userName = groupName,
|
||||||
|
password = "",
|
||||||
|
fullName = groupName,
|
||||||
|
mailAddress = groupName + "@devnull",
|
||||||
|
isAdmin = false,
|
||||||
|
url = url,
|
||||||
|
registeredDate = currentDate,
|
||||||
|
updatedDate = currentDate,
|
||||||
|
lastLoginDate = None,
|
||||||
|
image = None,
|
||||||
|
isGroupAccount = true,
|
||||||
|
isRemoved = false)
|
||||||
|
|
||||||
|
def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit =
|
||||||
|
Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed)
|
||||||
|
|
||||||
|
def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = {
|
||||||
|
GroupMembers.filter(_.groupName === groupName.bind).delete
|
||||||
|
members.foreach { case (userName, isManager) =>
|
||||||
|
GroupMembers insert GroupMember (groupName, userName, isManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] =
|
||||||
|
GroupMembers
|
||||||
|
.filter(_.groupName === groupName.bind)
|
||||||
|
.sortBy(_.userName)
|
||||||
|
.list
|
||||||
|
|
||||||
|
def getGroupsByUserName(userName: String)(implicit s: Session): List[String] =
|
||||||
|
GroupMembers
|
||||||
|
.filter(_.userName === userName.bind)
|
||||||
|
.sortBy(_.groupName)
|
||||||
|
.map(_.groupName)
|
||||||
|
.list
|
||||||
|
|
||||||
|
def removeUserRelatedData(userName: String)(implicit s: Session): Unit = {
|
||||||
|
GroupMembers.filter(_.userName === userName.bind).delete
|
||||||
|
Collaborators.filter(_.collaboratorName === userName.bind).delete
|
||||||
|
Repositories.filter(_.userName === userName.bind).delete
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object AccountService extends AccountService
|
||||||
|
|||||||
179
src/main/scala/service/ActivityService.scala
Normal file
179
src/main/scala/service/ActivityService.scala
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
|
import model.Activity
|
||||||
|
|
||||||
|
trait ActivityService {
|
||||||
|
|
||||||
|
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
|
||||||
|
Activities
|
||||||
|
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||||
|
.filter { case (t1, t2) =>
|
||||||
|
if(isPublic){
|
||||||
|
(t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind)
|
||||||
|
} else {
|
||||||
|
(t1.activityUserName === activityUserName.bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortBy { case (t1, t2) => t1.activityId desc }
|
||||||
|
.map { case (t1, t2) => t1 }
|
||||||
|
.take(30)
|
||||||
|
.list
|
||||||
|
|
||||||
|
def getRecentActivities()(implicit s: Session): List[Activity] =
|
||||||
|
Activities
|
||||||
|
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||||
|
.filter { case (t1, t2) => t2.isPrivate === false.bind }
|
||||||
|
.sortBy { case (t1, t2) => t1.activityId desc }
|
||||||
|
.map { case (t1, t2) => t1 }
|
||||||
|
.take(30)
|
||||||
|
.list
|
||||||
|
|
||||||
|
def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] =
|
||||||
|
Activities
|
||||||
|
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||||
|
.filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) }
|
||||||
|
.sortBy { case (t1, t2) => t1.activityId desc }
|
||||||
|
.map { case (t1, t2) => t1 }
|
||||||
|
.take(30)
|
||||||
|
.list
|
||||||
|
|
||||||
|
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"create_repository",
|
||||||
|
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"open_issue",
|
||||||
|
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(title),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"close_issue",
|
||||||
|
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(title),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"close_issue",
|
||||||
|
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(title),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"reopen_issue",
|
||||||
|
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(title),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"comment_issue",
|
||||||
|
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(cut(comment, 200)),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"comment_issue",
|
||||||
|
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(cut(comment, 200)),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"create_wiki",
|
||||||
|
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
|
||||||
|
Some(pageName),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"edit_wiki",
|
||||||
|
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
|
||||||
|
Some(pageName + ":" + commitId),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
|
||||||
|
branchName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"push",
|
||||||
|
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
||||||
|
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
|
||||||
|
tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"create_tag",
|
||||||
|
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
|
||||||
|
tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"delete_tag",
|
||||||
|
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"create_branch",
|
||||||
|
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"delete_branch",
|
||||||
|
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"fork",
|
||||||
|
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
|
||||||
|
None,
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"open_pullreq",
|
||||||
|
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(title),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"merge_pullreq",
|
||||||
|
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||||
|
Some(message),
|
||||||
|
currentDate)
|
||||||
|
|
||||||
|
private def cut(value: String, length: Int): String =
|
||||||
|
if(value.length > length) value.substring(0, length) + "..." else value
|
||||||
|
}
|
||||||
@@ -1,284 +1,386 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
import scala.slick.jdbc.{StaticQuery => Q}
|
||||||
import Database.threadLocalSession
|
import Q.interpolation
|
||||||
import scala.slick.jdbc.{StaticQuery => Q}
|
|
||||||
import Q.interpolation
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
import model._
|
import model.{Issue, IssueComment, IssueLabel, Label}
|
||||||
import Issues._
|
import util.Implicits._
|
||||||
import util.Implicits._
|
import util.StringUtil._
|
||||||
|
|
||||||
trait IssuesService {
|
trait IssuesService {
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
|
|
||||||
def getIssue(owner: String, repository: String, issueId: String) =
|
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
|
||||||
if (issueId forall (_.isDigit))
|
if (issueId forall (_.isDigit))
|
||||||
Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
|
Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
|
||||||
else None
|
else None
|
||||||
|
|
||||||
def getComments(owner: String, repository: String, issueId: Int) =
|
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||||
Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list
|
IssueComments filter (_.byIssue(owner, repository, issueId)) list
|
||||||
|
|
||||||
def getComment(owner: String, repository: String, commentId: String) =
|
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||||
if (commentId forall (_.isDigit))
|
if (commentId forall (_.isDigit))
|
||||||
Query(IssueComments) filter { t =>
|
IssueComments filter { t =>
|
||||||
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
|
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
|
||||||
} firstOption
|
} firstOption
|
||||||
else None
|
else None
|
||||||
|
|
||||||
def getIssueLabels(owner: String, repository: String, issueId: Int) =
|
def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||||
IssueLabels
|
IssueLabels
|
||||||
.innerJoin(Labels).on { (t1, t2) =>
|
.innerJoin(Labels).on { (t1, t2) =>
|
||||||
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
|
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
|
||||||
}
|
}
|
||||||
.filter ( _._1.byIssue(owner, repository, issueId) )
|
.filter ( _._1.byIssue(owner, repository, issueId) )
|
||||||
.map ( _._2 )
|
.map ( _._2 )
|
||||||
.list
|
.list
|
||||||
|
|
||||||
/**
|
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
|
||||||
* Returns the count of the search result against issues.
|
IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
|
||||||
*
|
|
||||||
* @param owner the repository owner
|
/**
|
||||||
* @param repository the repository name
|
* Returns the count of the search result against issues.
|
||||||
* @param condition the search condition
|
*
|
||||||
* @param filter the filter type ("all", "assigned" or "created_by")
|
* @param condition the search condition
|
||||||
* @param userName the filter user name required for "assigned" and "created_by"
|
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||||
* @return the count of the search result
|
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
|
||||||
*/
|
* @param repos Tuple of the repository owner and the repository name
|
||||||
def countIssue(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]): Int = {
|
* @return the count of the search result
|
||||||
// TODO It must be _.length instead of map (_.issueId) list).length.
|
*/
|
||||||
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
|
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||||
// https://github.com/slick/slick/issues/170
|
repos: (String, String)*)(implicit s: Session): Int =
|
||||||
(searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length
|
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Returns the Map which contains issue count for each labels.
|
* Returns the Map which contains issue count for each labels.
|
||||||
*
|
*
|
||||||
* @param owner the repository owner
|
* @param owner the repository owner
|
||||||
* @param repository the repository name
|
* @param repository the repository name
|
||||||
* @param condition the search condition
|
* @param condition the search condition
|
||||||
* @param filter the filter type ("all", "assigned" or "created_by")
|
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||||
* @param userName the filter user name required for "assigned" and "created_by"
|
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
|
||||||
* @return the Map which contains issue count for each labels (key is label name, value is issue count),
|
*/
|
||||||
*/
|
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
|
||||||
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
|
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
|
||||||
filter: String, userName: Option[String]): Map[String, Int] = {
|
|
||||||
|
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
|
||||||
searchIssueQuery(owner, repository, condition.copy(labels = Set.empty), filter, userName)
|
.innerJoin(IssueLabels).on { (t1, t2) =>
|
||||||
.innerJoin(IssueLabels).on { (t1, t2) =>
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
}
|
||||||
}
|
.innerJoin(Labels).on { case ((t1, t2), t3) =>
|
||||||
.innerJoin(Labels).on { case ((t1, t2), t3) =>
|
t2.byLabel(t3.userName, t3.repositoryName, t3.labelId)
|
||||||
t2.byLabel(t3.userName, t3.repositoryName, t3.labelId)
|
}
|
||||||
}
|
.groupBy { case ((t1, t2), t3) =>
|
||||||
.groupBy { case ((t1, t2), t3) =>
|
t3.labelName
|
||||||
t3.labelName
|
}
|
||||||
}
|
.map { case (labelName, t) =>
|
||||||
.map { case (labelName, t) =>
|
labelName -> t.length
|
||||||
labelName ~ t.length
|
}
|
||||||
}
|
.toMap
|
||||||
.toMap
|
}
|
||||||
}
|
/**
|
||||||
|
* Returns list which contains issue count for each repository.
|
||||||
/**
|
* If the issue does not exist, its repository is not included in the result.
|
||||||
* Returns the search result against issues.
|
*
|
||||||
*
|
* @param condition the search condition
|
||||||
* @param owner the repository owner
|
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||||
* @param repository the repository name
|
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
||||||
* @param condition the search condition
|
* @param repos Tuple of the repository owner and the repository name
|
||||||
* @param filter the filter type ("all", "assigned" or "created_by")
|
* @return list which contains issue count for each repository
|
||||||
* @param userName the filter user name required for "assigned" and "created_by"
|
*/
|
||||||
* @param offset the offset for pagination
|
def countIssueGroupByRepository(
|
||||||
* @param limit the limit for pagination
|
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||||
* @return the search result (list of tuples which contain issue, labels and comment count)
|
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
|
||||||
*/
|
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
|
||||||
def searchIssue(owner: String, repository: String, condition: IssueSearchCondition,
|
.groupBy { t =>
|
||||||
filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = {
|
t.userName -> t.repositoryName
|
||||||
|
}
|
||||||
// get issues and comment count
|
.map { case (repo, t) =>
|
||||||
val issues = searchIssueQuery(owner, repository, condition, filter, userName)
|
(repo._1, repo._2, t.length)
|
||||||
.leftJoin(Query(IssueComments)
|
}
|
||||||
.filter { _.byRepository(owner, repository) }
|
.sortBy(_._3 desc)
|
||||||
.groupBy { _.issueId }
|
.list
|
||||||
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
|
}
|
||||||
.sortBy { case (t1, t2) =>
|
|
||||||
(condition.sort match {
|
/**
|
||||||
case "created" => t1.registeredDate
|
* Returns the search result against issues.
|
||||||
case "comments" => t2._2
|
*
|
||||||
case "updated" => t1.updatedDate
|
* @param condition the search condition
|
||||||
}) match {
|
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
|
||||||
case sort => condition.direction match {
|
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
||||||
case "asc" => sort asc
|
* @param offset the offset for pagination
|
||||||
case "desc" => sort desc
|
* @param limit the limit for pagination
|
||||||
}
|
* @param repos Tuple of the repository owner and the repository name
|
||||||
}
|
* @return the search result (list of tuples which contain issue, labels and comment count)
|
||||||
}
|
*/
|
||||||
.map { case (t1, t2) => (t1, t2._2.ifNull(0)) }
|
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||||
.drop(offset).take(limit)
|
offset: Int, limit: Int, repos: (String, String)*)
|
||||||
.list
|
(implicit s: Session): List[(Issue, List[Label], Int)] = {
|
||||||
|
|
||||||
// get labels
|
// get issues and comment count and labels
|
||||||
val labels = Query(IssueLabels)
|
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
|
||||||
.innerJoin(Labels).on { (t1, t2) =>
|
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
||||||
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
|
.sortBy { case (t1, t2) =>
|
||||||
}
|
(condition.sort match {
|
||||||
.filter { case (t1, t2) =>
|
case "created" => t1.registeredDate
|
||||||
(t1.byRepository(owner, repository)) &&
|
case "comments" => t2.commentCount
|
||||||
(t1.issueId inSetBind (issues.map(_._1.issueId)))
|
case "updated" => t1.updatedDate
|
||||||
}
|
}) match {
|
||||||
.sortBy { case (t1, t2) => t1.issueId ~ t2.labelName }
|
case sort => condition.direction match {
|
||||||
.map { case (t1, t2) => (t1.issueId, t2) }
|
case "asc" => sort asc
|
||||||
.list
|
case "desc" => sort desc
|
||||||
|
}
|
||||||
issues.map { case (issue, commentCount) =>
|
}
|
||||||
(issue, labels.collect { case (issueId, labels) if(issueId == issue.issueId) => labels }, commentCount)
|
}
|
||||||
}
|
.drop(offset).take(limit)
|
||||||
}
|
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
||||||
|
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
|
||||||
/**
|
.map { case (((t1, t2), t3), t4) =>
|
||||||
* Assembles query for conditional issue searching.
|
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
||||||
*/
|
}
|
||||||
private def searchIssueQuery(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]) =
|
.list
|
||||||
Query(Issues) filter { t1 =>
|
.splitWith { (c1, c2) =>
|
||||||
(t1.byRepository(owner, repository)) &&
|
c1._1.userName == c2._1.userName &&
|
||||||
(t1.closed is (condition.state == "closed").bind) &&
|
c1._1.repositoryName == c2._1.repositoryName &&
|
||||||
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
c1._1.issueId == c2._1.issueId
|
||||||
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
|
}
|
||||||
(t1.assignedUserName is userName.get.bind, filter == "assigned") &&
|
.map { issues => issues.head match {
|
||||||
(t1.openedUserName is userName.get.bind, filter == "created_by") &&
|
case (issue, commentCount, _,_,_) =>
|
||||||
(IssueLabels filter { t2 =>
|
(issue,
|
||||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
|
issues.flatMap { t => t._3.map (
|
||||||
(t2.labelId in
|
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
|
||||||
(Labels filter { t3 =>
|
)} toList,
|
||||||
(t3.byRepository(t1.userName, t1.repositoryName)) &&
|
commentCount)
|
||||||
(t3.labelName inSetBind condition.labels)
|
}} toList
|
||||||
} map(_.labelId)))
|
}
|
||||||
} exists, condition.labels.nonEmpty)
|
|
||||||
}
|
/**
|
||||||
|
* Assembles query for conditional issue searching.
|
||||||
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
*/
|
||||||
assignedUserName: Option[String], milestoneId: Option[Int]) =
|
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
|
||||||
// next id number
|
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
|
||||||
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
|
Issues filter { t1 =>
|
||||||
.firstOption.filter { id =>
|
condition.repo
|
||||||
Issues insert Issue(
|
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
|
||||||
owner,
|
.getOrElse (repos)
|
||||||
repository,
|
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
||||||
id,
|
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
||||||
loginUser,
|
(t1.closed === (condition.state == "closed").bind) &&
|
||||||
milestoneId,
|
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
||||||
assignedUserName,
|
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
|
||||||
title,
|
(t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
|
||||||
content,
|
(t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
|
||||||
false,
|
(t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
|
||||||
currentDate,
|
(t1.pullRequest === true.bind, onlyPullRequest) &&
|
||||||
currentDate)
|
(IssueLabels filter { t2 =>
|
||||||
|
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
|
||||||
// increment issue id
|
(t2.labelId in
|
||||||
IssueId
|
(Labels filter { t3 =>
|
||||||
.filter (_.byPrimaryKey(owner, repository))
|
(t3.byRepository(t1.userName, t1.repositoryName)) &&
|
||||||
.map (_.issueId)
|
(t3.labelName inSetBind condition.labels)
|
||||||
.update (id) > 0
|
} map(_.labelId)))
|
||||||
} get
|
} exists, condition.labels.nonEmpty)
|
||||||
|
}
|
||||||
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
|
|
||||||
IssueLabels insert (IssueLabel(owner, repository, issueId, labelId))
|
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
||||||
|
assignedUserName: Option[String], milestoneId: Option[Int],
|
||||||
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
|
isPullRequest: Boolean = false)(implicit s: Session) =
|
||||||
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
|
// next id number
|
||||||
|
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
|
||||||
def createComment(owner: String, repository: String, loginUser: String,
|
.firstOption.filter { id =>
|
||||||
issueId: Int, content: String, action: Option[String]) =
|
Issues insert Issue(
|
||||||
IssueComments.autoInc insert (
|
owner,
|
||||||
owner,
|
repository,
|
||||||
repository,
|
id,
|
||||||
issueId,
|
loginUser,
|
||||||
action,
|
milestoneId,
|
||||||
loginUser,
|
assignedUserName,
|
||||||
content,
|
title,
|
||||||
currentDate,
|
content,
|
||||||
currentDate)
|
false,
|
||||||
|
currentDate,
|
||||||
def updateIssue(owner: String, repository: String, issueId: Int,
|
currentDate,
|
||||||
title: String, content: Option[String]) =
|
isPullRequest)
|
||||||
Issues
|
|
||||||
.filter (_.byPrimaryKey(owner, repository, issueId))
|
// increment issue id
|
||||||
.map { t =>
|
IssueId
|
||||||
t.title ~ t.content.? ~ t.updatedDate
|
.filter (_.byPrimaryKey(owner, repository))
|
||||||
}
|
.map (_.issueId)
|
||||||
.update (title, content, currentDate)
|
.update (id) > 0
|
||||||
|
} get
|
||||||
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) =
|
|
||||||
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
|
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
|
||||||
|
IssueLabels insert IssueLabel(owner, repository, issueId, labelId)
|
||||||
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) =
|
|
||||||
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
|
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
|
||||||
|
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
|
||||||
def updateComment(commentId: Int, content: String) =
|
|
||||||
IssueComments
|
def createComment(owner: String, repository: String, loginUser: String,
|
||||||
.filter (_.byPrimaryKey(commentId))
|
issueId: Int, content: String, action: String)(implicit s: Session): Int =
|
||||||
.map { t =>
|
IssueComments.autoInc insert IssueComment(
|
||||||
t.content ~ t.updatedDate
|
userName = owner,
|
||||||
}
|
repositoryName = repository,
|
||||||
.update (content, currentDate)
|
issueId = issueId,
|
||||||
|
action = action,
|
||||||
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
|
commentedUserName = loginUser,
|
||||||
Issues
|
content = content,
|
||||||
.filter (_.byPrimaryKey(owner, repository, issueId))
|
registeredDate = currentDate,
|
||||||
.map { t =>
|
updatedDate = currentDate)
|
||||||
t.closed ~ t.updatedDate
|
|
||||||
}
|
def updateIssue(owner: String, repository: String, issueId: Int,
|
||||||
.update (closed, currentDate)
|
title: String, content: Option[String])(implicit s: Session) =
|
||||||
|
Issues
|
||||||
}
|
.filter (_.byPrimaryKey(owner, repository, issueId))
|
||||||
|
.map { t =>
|
||||||
object IssuesService {
|
(t.title, t.content.?, t.updatedDate)
|
||||||
import java.net.URLEncoder
|
}
|
||||||
import javax.servlet.http.HttpServletRequest
|
.update (title, content, currentDate)
|
||||||
|
|
||||||
val IssueLimit = 30
|
def updateAssignedUserName(owner: String, repository: String, issueId: Int,
|
||||||
|
assignedUserName: Option[String])(implicit s: Session) =
|
||||||
case class IssueSearchCondition(
|
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
|
||||||
labels: Set[String] = Set.empty,
|
|
||||||
milestoneId: Option[Option[Int]] = None,
|
def updateMilestoneId(owner: String, repository: String, issueId: Int,
|
||||||
state: String = "open",
|
milestoneId: Option[Int])(implicit s: Session) =
|
||||||
sort: String = "created",
|
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
|
||||||
direction: String = "desc"){
|
|
||||||
|
def updateComment(commentId: Int, content: String)(implicit s: Session) =
|
||||||
import IssueSearchCondition._
|
IssueComments
|
||||||
|
.filter (_.byPrimaryKey(commentId))
|
||||||
def toURL: String =
|
.map { t =>
|
||||||
"?" + List(
|
t.content -> t.updatedDate
|
||||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
|
}
|
||||||
milestoneId.map { id => "milestone=" + (id match {
|
.update (content, currentDate)
|
||||||
case Some(x) => x.toString
|
|
||||||
case None => "none"
|
def deleteComment(commentId: Int)(implicit s: Session) =
|
||||||
})},
|
IssueComments filter (_.byPrimaryKey(commentId)) delete
|
||||||
Some("state=" + urlEncode(state)),
|
|
||||||
Some("sort=" + urlEncode(sort)),
|
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) =
|
||||||
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
|
Issues
|
||||||
|
.filter (_.byPrimaryKey(owner, repository, issueId))
|
||||||
}
|
.map { t =>
|
||||||
|
t.closed -> t.updatedDate
|
||||||
object IssueSearchCondition {
|
}
|
||||||
|
.update (closed, currentDate)
|
||||||
private def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
|
|
||||||
|
/**
|
||||||
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
|
* Search issues by keyword.
|
||||||
val value = request.getParameter(name)
|
*
|
||||||
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
* @param owner the repository owner
|
||||||
}
|
* @param repository the repository name
|
||||||
|
* @param query the keywords separated by whitespace.
|
||||||
def apply(request: HttpServletRequest): IssueSearchCondition =
|
* @return issues with comment count and matched content of issue or comment
|
||||||
IssueSearchCondition(
|
*/
|
||||||
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
|
def searchIssuesByKeyword(owner: String, repository: String, query: String)
|
||||||
param(request, "milestone").map(_ match {
|
(implicit s: Session): List[(Issue, Int, String)] = {
|
||||||
case "none" => None
|
import slick.driver.JdbcDriver.likeEncode
|
||||||
case x => Some(x.toInt)
|
val keywords = splitWords(query.toLowerCase)
|
||||||
}),
|
|
||||||
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
// Search Issue
|
||||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
val issues = Issues
|
||||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
.innerJoin(IssueOutline).on { case (t1, t2) =>
|
||||||
}
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
}
|
}
|
||||||
|
.filter { case (t1, t2) =>
|
||||||
|
keywords.map { keyword =>
|
||||||
|
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
|
||||||
|
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
|
||||||
|
} .reduceLeft(_ && _)
|
||||||
|
}
|
||||||
|
.map { case (t1, t2) =>
|
||||||
|
(t1, 0, t1.content.?, t2.commentCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search IssueComment
|
||||||
|
val comments = IssueComments
|
||||||
|
.innerJoin(Issues).on { case (t1, t2) =>
|
||||||
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
|
}
|
||||||
|
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
|
||||||
|
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
|
||||||
|
}
|
||||||
|
.filter { case ((t1, t2), t3) =>
|
||||||
|
keywords.map { query =>
|
||||||
|
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
|
||||||
|
}.reduceLeft(_ && _)
|
||||||
|
}
|
||||||
|
.map { case ((t1, t2), t3) =>
|
||||||
|
(t2, t1.commentId, t1.content.?, t3.commentCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
issues.union(comments).sortBy { case (issue, commentId, _, _) =>
|
||||||
|
issue.issueId -> commentId
|
||||||
|
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
|
||||||
|
issue1.issueId == issue2.issueId
|
||||||
|
}.map { _.head match {
|
||||||
|
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
|
||||||
|
}
|
||||||
|
}.toList
|
||||||
|
}
|
||||||
|
|
||||||
|
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = {
|
||||||
|
extractCloseId(message).foreach { issueId =>
|
||||||
|
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
|
||||||
|
createComment(owner, repository, userName, issue.issueId, "Close", "close")
|
||||||
|
updateClosed(owner, repository, issue.issueId, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object IssuesService {
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
|
val IssueLimit = 30
|
||||||
|
|
||||||
|
case class IssueSearchCondition(
|
||||||
|
labels: Set[String] = Set.empty,
|
||||||
|
milestoneId: Option[Option[Int]] = None,
|
||||||
|
repo: Option[String] = None,
|
||||||
|
state: String = "open",
|
||||||
|
sort: String = "created",
|
||||||
|
direction: String = "desc"){
|
||||||
|
|
||||||
|
def toURL: String =
|
||||||
|
"?" + List(
|
||||||
|
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
||||||
|
milestoneId.map { id => "milestone=" + (id match {
|
||||||
|
case Some(x) => x.toString
|
||||||
|
case None => "none"
|
||||||
|
})},
|
||||||
|
repo.map("for=" + urlEncode(_)),
|
||||||
|
Some("state=" + urlEncode(state)),
|
||||||
|
Some("sort=" + urlEncode(sort)),
|
||||||
|
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object IssueSearchCondition {
|
||||||
|
|
||||||
|
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
|
||||||
|
val value = request.getParameter(name)
|
||||||
|
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply(request: HttpServletRequest): IssueSearchCondition =
|
||||||
|
IssueSearchCondition(
|
||||||
|
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
||||||
|
param(request, "milestone").map{
|
||||||
|
case "none" => None
|
||||||
|
case x => x.toIntOpt
|
||||||
|
},
|
||||||
|
param(request, "for"),
|
||||||
|
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
||||||
|
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||||
|
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
||||||
|
|
||||||
|
def page(request: HttpServletRequest) = try {
|
||||||
|
val i = param(request, "page").getOrElse("1").toInt
|
||||||
|
if(i <= 0) 1 else i
|
||||||
|
} catch {
|
||||||
|
case e: NumberFormatException => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
import model.Profile._
|
||||||
import Database.threadLocalSession
|
import profile.simple._
|
||||||
|
import model.Label
|
||||||
import model._
|
|
||||||
|
|
||||||
trait LabelsService {
|
trait LabelsService {
|
||||||
|
|
||||||
def getLabels(owner: String, repository: String): List[Label] =
|
def getLabels(owner: String, repository: String)(implicit s: Session): List[Label] =
|
||||||
Query(Labels).filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list
|
Labels.filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list
|
||||||
|
|
||||||
def getLabel(owner: String, repository: String, labelId: Int): Option[Label] =
|
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
|
||||||
Query(Labels).filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
|
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
|
||||||
|
|
||||||
def createLabel(owner: String, repository: String, labelName: String, color: String): Unit =
|
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit =
|
||||||
Labels.ins insert (owner, repository, labelName, color)
|
Labels insert Label(
|
||||||
|
userName = owner,
|
||||||
|
repositoryName = repository,
|
||||||
|
labelName = labelName,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
|
||||||
def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String): Unit =
|
def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String)
|
||||||
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).map(t => t.labelName ~ t.color)
|
(implicit s: Session): Unit =
|
||||||
.update(labelName, color)
|
Labels.filter(_.byPrimaryKey(owner, repository, labelId))
|
||||||
|
.map(t => t.labelName -> t.color)
|
||||||
|
.update(labelName, color)
|
||||||
|
|
||||||
def deleteLabel(owner: String, repository: String, labelId: Int): Unit = {
|
def deleteLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Unit = {
|
||||||
IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete
|
IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete
|
||||||
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete
|
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,49 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import scala.slick.driver.H2Driver.simple._
|
import model.Profile._
|
||||||
import Database.threadLocalSession
|
import profile.simple._
|
||||||
|
import model.Milestone
|
||||||
import model._
|
// TODO [Slick 2.0]NOT import directly?
|
||||||
import Milestones._
|
import model.Profile.dateColumnType
|
||||||
|
|
||||||
trait MilestonesService {
|
trait MilestonesService {
|
||||||
|
|
||||||
def createMilestone(owner: String, repository: String, title: String, description: Option[String],
|
def createMilestone(owner: String, repository: String, title: String, description: Option[String],
|
||||||
dueDate: Option[java.util.Date]): Unit =
|
dueDate: Option[java.util.Date])(implicit s: Session): Unit =
|
||||||
Milestones.ins insert (owner, repository, title, description, dueDate, None)
|
Milestones insert Milestone(
|
||||||
|
userName = owner,
|
||||||
|
repositoryName = repository,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
dueDate = dueDate,
|
||||||
|
closedDate = None
|
||||||
|
)
|
||||||
|
|
||||||
def updateMilestone(milestone: Milestone): Unit =
|
def updateMilestone(milestone: Milestone)(implicit s: Session): Unit =
|
||||||
Milestones
|
Milestones
|
||||||
.filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId))
|
.filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId))
|
||||||
.map (t => t.title ~ t.description.? ~ t.dueDate.? ~ t.closedDate.?)
|
.map (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?))
|
||||||
.update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate)
|
.update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate)
|
||||||
|
|
||||||
def openMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = None))
|
def openMilestone(milestone: Milestone)(implicit s: Session): Unit =
|
||||||
|
updateMilestone(milestone.copy(closedDate = None))
|
||||||
|
|
||||||
def closeMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = Some(currentDate)))
|
def closeMilestone(milestone: Milestone)(implicit s: Session): Unit =
|
||||||
|
updateMilestone(milestone.copy(closedDate = Some(currentDate)))
|
||||||
|
|
||||||
def deleteMilestone(owner: String, repository: String, milestoneId: Int): Unit = {
|
def deleteMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Unit = {
|
||||||
Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None)
|
Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None)
|
||||||
Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete
|
Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete
|
||||||
}
|
}
|
||||||
|
|
||||||
def getMilestone(owner: String, repository: String, milestoneId: Int): Option[Milestone] =
|
def getMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Option[Milestone] =
|
||||||
Query(Milestones).filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption
|
Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption
|
||||||
|
|
||||||
def getMilestonesWithIssueCount(owner: String, repository: String): List[(Milestone, Int, Int)] = {
|
def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
|
||||||
val counts = Issues
|
val counts = Issues
|
||||||
.filter { t => (t.byRepository(owner, repository)) && (t.milestoneId isNotNull) }
|
.filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) }
|
||||||
.groupBy { t => t.milestoneId ~ t.closed }
|
.groupBy { t => t.milestoneId -> t.closed }
|
||||||
.map { case (t1, t2) => (t1._1 ~ t1._2) -> t2.length }
|
.map { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
|
||||||
.toMap
|
.toMap
|
||||||
|
|
||||||
getMilestones(owner, repository).map { milestone =>
|
getMilestones(owner, repository).map { milestone =>
|
||||||
@@ -42,6 +51,7 @@ trait MilestonesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getMilestones(owner: String, repository: String): List[Milestone] =
|
def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] =
|
||||||
Query(Milestones).filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
|
Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/main/scala/service/PullRequestService.scala
Normal file
74
src/main/scala/service/PullRequestService.scala
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
|
import model.{PullRequest, Issue}
|
||||||
|
|
||||||
|
trait PullRequestService { self: IssuesService =>
|
||||||
|
import PullRequestService._
|
||||||
|
|
||||||
|
def getPullRequest(owner: String, repository: String, issueId: Int)
|
||||||
|
(implicit s: Session): Option[(Issue, PullRequest)] =
|
||||||
|
getIssue(owner, repository, issueId.toString).flatMap{ issue =>
|
||||||
|
PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
|
||||||
|
pullreq => (issue, pullreq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
PullRequests.filter(_.byPrimaryKey(owner, repository, issueId))
|
||||||
|
.map(pr => pr.commitIdTo -> pr.commitIdFrom)
|
||||||
|
.update((commitIdTo, commitIdFrom))
|
||||||
|
|
||||||
|
def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])
|
||||||
|
(implicit s: Session): List[PullRequestCount] =
|
||||||
|
PullRequests
|
||||||
|
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||||
|
.filter { case (t1, t2) =>
|
||||||
|
(t2.closed === closed.bind) &&
|
||||||
|
(t1.userName === owner.get.bind, owner.isDefined) &&
|
||||||
|
(t1.repositoryName === repository.get.bind, repository.isDefined)
|
||||||
|
}
|
||||||
|
.groupBy { case (t1, t2) => t2.openedUserName }
|
||||||
|
.map { case (userName, t) => userName -> t.length }
|
||||||
|
.sortBy(_._2 desc)
|
||||||
|
.list
|
||||||
|
.map { x => PullRequestCount(x._1, x._2) }
|
||||||
|
|
||||||
|
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
|
||||||
|
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||||
|
commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit =
|
||||||
|
PullRequests insert PullRequest(
|
||||||
|
originUserName,
|
||||||
|
originRepositoryName,
|
||||||
|
issueId,
|
||||||
|
originBranch,
|
||||||
|
requestUserName,
|
||||||
|
requestRepositoryName,
|
||||||
|
requestBranch,
|
||||||
|
commitIdFrom,
|
||||||
|
commitIdTo)
|
||||||
|
|
||||||
|
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean)
|
||||||
|
(implicit s: Session): List[PullRequest] =
|
||||||
|
PullRequests
|
||||||
|
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||||
|
.filter { case (t1, t2) =>
|
||||||
|
(t1.requestUserName === userName.bind) &&
|
||||||
|
(t1.requestRepositoryName === repositoryName.bind) &&
|
||||||
|
(t1.requestBranch === branch.bind) &&
|
||||||
|
(t2.closed === closed.bind)
|
||||||
|
}
|
||||||
|
.map { case (t1, t2) => t1 }
|
||||||
|
.list
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object PullRequestService {
|
||||||
|
|
||||||
|
val PullRequestLimit = 25
|
||||||
|
|
||||||
|
case class PullRequestCount(userName: String, count: Int)
|
||||||
|
|
||||||
|
}
|
||||||
126
src/main/scala/service/RepositorySearchService.scala
Normal file
126
src/main/scala/service/RepositorySearchService.scala
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import util.{FileUtil, StringUtil, JGitUtil}
|
||||||
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk
|
||||||
|
import org.eclipse.jgit.treewalk.TreeWalk
|
||||||
|
import org.eclipse.jgit.lib.FileMode
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
trait RepositorySearchService { self: IssuesService =>
|
||||||
|
import RepositorySearchService._
|
||||||
|
|
||||||
|
def countIssues(owner: String, repository: String, query: String)(implicit session: Session): Int =
|
||||||
|
searchIssuesByKeyword(owner, repository, query).length
|
||||||
|
|
||||||
|
def searchIssues(owner: String, repository: String, query: String)(implicit session: Session): List[IssueSearchResult] =
|
||||||
|
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
||||||
|
IssueSearchResult(
|
||||||
|
issue.issueId,
|
||||||
|
issue.title,
|
||||||
|
issue.openedUserName,
|
||||||
|
issue.registeredDate,
|
||||||
|
commentCount,
|
||||||
|
getHighlightText(content, query)._1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def countFiles(owner: String, repository: String, query: String): Int =
|
||||||
|
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
||||||
|
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
|
||||||
|
}
|
||||||
|
|
||||||
|
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
|
||||||
|
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
||||||
|
if(JGitUtil.isEmpty(git)){
|
||||||
|
Nil
|
||||||
|
} else {
|
||||||
|
val files = searchRepositoryFiles(git, query)
|
||||||
|
val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD")
|
||||||
|
files.map { case (path, text) =>
|
||||||
|
val (highlightText, lineNumber) = getHighlightText(text, query)
|
||||||
|
FileSearchResult(
|
||||||
|
path,
|
||||||
|
commits(path).getCommitterIdent.getWhen,
|
||||||
|
highlightText,
|
||||||
|
lineNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
||||||
|
val revWalk = new RevWalk(git.getRepository)
|
||||||
|
val objectId = git.getRepository.resolve("HEAD")
|
||||||
|
val revCommit = revWalk.parseCommit(objectId)
|
||||||
|
val treeWalk = new TreeWalk(git.getRepository)
|
||||||
|
treeWalk.setRecursive(true)
|
||||||
|
treeWalk.addTree(revCommit.getTree)
|
||||||
|
|
||||||
|
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||||
|
val list = new scala.collection.mutable.ListBuffer[(String, String)]
|
||||||
|
|
||||||
|
while (treeWalk.next()) {
|
||||||
|
val mode = treeWalk.getFileMode(0)
|
||||||
|
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
|
||||||
|
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
|
||||||
|
if(FileUtil.isText(bytes)){
|
||||||
|
val text = StringUtil.convertFromByteArray(bytes)
|
||||||
|
val lowerText = text.toLowerCase
|
||||||
|
val indices = keywords.map(lowerText.indexOf _)
|
||||||
|
if(!indices.exists(_ < 0)){
|
||||||
|
list.append((treeWalk.getPathString, text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
treeWalk.release
|
||||||
|
revWalk.release
|
||||||
|
|
||||||
|
list.toList
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object RepositorySearchService {
|
||||||
|
|
||||||
|
val CodeLimit = 10
|
||||||
|
val IssueLimit = 10
|
||||||
|
|
||||||
|
def getHighlightText(content: String, query: String): (String, Int) = {
|
||||||
|
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||||
|
val lowerText = content.toLowerCase
|
||||||
|
val indices = keywords.map(lowerText.indexOf _)
|
||||||
|
|
||||||
|
if(!indices.exists(_ < 0)){
|
||||||
|
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
|
||||||
|
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
|
||||||
|
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
|
||||||
|
"<span class=\"highlight\">$1</span>")
|
||||||
|
(highlightText, lineNumber + 1)
|
||||||
|
} else {
|
||||||
|
(content.split("\n").take(5).mkString("\n"), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class SearchResult(
|
||||||
|
files : List[(String, String)],
|
||||||
|
issues: List[(model.Issue, Int, String)])
|
||||||
|
|
||||||
|
case class IssueSearchResult(
|
||||||
|
issueId: Int,
|
||||||
|
title: String,
|
||||||
|
openedUserName: String,
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
commentCount: Int,
|
||||||
|
highlightText: String)
|
||||||
|
|
||||||
|
case class FileSearchResult(
|
||||||
|
path: String,
|
||||||
|
lastModified: java.util.Date,
|
||||||
|
highlightText: String,
|
||||||
|
highlightLineNumber: Int)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import model._
|
import model.Profile._
|
||||||
import Repositories._
|
import profile.simple._
|
||||||
import scala.slick.driver.H2Driver.simple._
|
import model.{Repository, Account, Collaborator}
|
||||||
import Database.threadLocalSession
|
|
||||||
import util.JGitUtil
|
import util.JGitUtil
|
||||||
|
|
||||||
trait RepositoryService { self: AccountService =>
|
trait RepositoryService { self: AccountService =>
|
||||||
@@ -12,32 +11,121 @@ trait RepositoryService { self: AccountService =>
|
|||||||
/**
|
/**
|
||||||
* Creates a new repository.
|
* Creates a new repository.
|
||||||
*
|
*
|
||||||
* The project is created as public repository at first. Users can modify the project type at the repository settings
|
|
||||||
* page after the project creation to configure the project as the private repository.
|
|
||||||
*
|
|
||||||
* @param repositoryName the repository name
|
* @param repositoryName the repository name
|
||||||
* @param userName the user name of the repository owner
|
* @param userName the user name of the repository owner
|
||||||
* @param description the repository description
|
* @param description the repository description
|
||||||
|
* @param isPrivate the repository type (private is true, otherwise false)
|
||||||
|
* @param originRepositoryName specify for the forked repository. (default is None)
|
||||||
|
* @param originUserName specify for the forked repository. (default is None)
|
||||||
*/
|
*/
|
||||||
def createRepository(repositoryName: String, userName: String, description: Option[String]): Unit = {
|
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
|
||||||
|
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
|
||||||
|
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
|
||||||
|
(implicit s: Session): Unit = {
|
||||||
Repositories insert
|
Repositories insert
|
||||||
Repository(
|
Repository(
|
||||||
userName = userName,
|
userName = userName,
|
||||||
repositoryName = repositoryName,
|
repositoryName = repositoryName,
|
||||||
isPrivate = false,
|
isPrivate = isPrivate,
|
||||||
description = description,
|
description = description,
|
||||||
defaultBranch = "master",
|
defaultBranch = "master",
|
||||||
registeredDate = currentDate,
|
registeredDate = currentDate,
|
||||||
updatedDate = currentDate,
|
updatedDate = currentDate,
|
||||||
lastActivityDate = currentDate)
|
lastActivityDate = currentDate,
|
||||||
|
originUserName = originUserName,
|
||||||
|
originRepositoryName = originRepositoryName,
|
||||||
|
parentUserName = parentUserName,
|
||||||
|
parentRepositoryName = parentRepositoryName)
|
||||||
|
|
||||||
IssueId insert (userName, repositoryName, 0)
|
IssueId insert (userName, repositoryName, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
def deleteRepository(userName: String, repositoryName: String): Unit = {
|
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String)
|
||||||
|
(implicit s: Session): Unit = {
|
||||||
|
getAccountByUserName(newUserName).foreach { account =>
|
||||||
|
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
|
||||||
|
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
|
||||||
|
|
||||||
|
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
|
||||||
|
Repositories.filter { t =>
|
||||||
|
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
|
||||||
|
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
|
||||||
|
|
||||||
|
Repositories.filter { t =>
|
||||||
|
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
|
||||||
|
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
|
||||||
|
|
||||||
|
PullRequests.filter { t =>
|
||||||
|
t.requestRepositoryName === oldRepositoryName.bind
|
||||||
|
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||||
|
|
||||||
|
deleteRepository(oldUserName, oldRepositoryName)
|
||||||
|
|
||||||
|
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||||
|
|
||||||
|
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
||||||
|
Issues.insertAll(issues.map { x => x.copy(
|
||||||
|
userName = newUserName,
|
||||||
|
repositoryName = newRepositoryName,
|
||||||
|
milestoneId = x.milestoneId.map { id =>
|
||||||
|
newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId
|
||||||
|
}
|
||||||
|
)} :_*)
|
||||||
|
|
||||||
|
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
if(account.isGroupAccount){
|
||||||
|
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
|
||||||
|
} else {
|
||||||
|
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update activity messages
|
||||||
|
val updateActivities = Activities.filter { t =>
|
||||||
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
|
||||||
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
|
||||||
|
}.map { t => t.activityId -> t.message }.list
|
||||||
|
|
||||||
|
updateActivities.foreach { case (activityId, message) =>
|
||||||
|
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
|
||||||
|
message
|
||||||
|
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
|
||||||
|
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
|
||||||
|
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
|
||||||
|
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
|
||||||
|
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||||
|
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,42 +135,8 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* @param userName the user name of repository owner
|
* @param userName the user name of repository owner
|
||||||
* @return the list of repository names
|
* @return the list of repository names
|
||||||
*/
|
*/
|
||||||
def getRepositoryNamesOfUser(userName: String): List[String] =
|
def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] =
|
||||||
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
|
Repositories filter(_.userName === userName.bind) map (_.repositoryName) list
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of specified user's repositories information.
|
|
||||||
*
|
|
||||||
* @param userName the user name
|
|
||||||
* @param baseUrl the base url of this application
|
|
||||||
* @param loginUserName the logged in user name
|
|
||||||
* @return the list of repository information which is sorted in descending order of lastActivityDate.
|
|
||||||
*/
|
|
||||||
def getVisibleRepositories(userName: String, baseUrl: String, loginUserName: Option[String]): List[RepositoryInfo] = {
|
|
||||||
val q1 = Repositories
|
|
||||||
.filter { t => t.userName is userName.bind }
|
|
||||||
.map { r => r }
|
|
||||||
|
|
||||||
val q2 = Collaborators
|
|
||||||
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
|
||||||
.filter{ case (t1, t2) => t1.collaboratorName is userName.bind}
|
|
||||||
.map { case (t1, t2) => t2 }
|
|
||||||
|
|
||||||
def visibleFor(t: Repositories.type, loginUserName: Option[String]) = {
|
|
||||||
loginUserName match {
|
|
||||||
case Some(x) => (t.isPrivate is false.bind) || (
|
|
||||||
(t.isPrivate is true.bind) && ((t.userName is x.bind) || (Collaborators.filter { c =>
|
|
||||||
c.byRepository(t.userName, t.repositoryName) && (c.collaboratorName is x.bind)
|
|
||||||
}.exists)))
|
|
||||||
case None => (t.isPrivate is false.bind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
|
|
||||||
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
|
||||||
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the specified repository information.
|
* Returns the specified repository information.
|
||||||
@@ -92,53 +146,115 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* @param baseUrl the base url of this application
|
* @param baseUrl the base url of this application
|
||||||
* @return the repository information
|
* @return the repository information
|
||||||
*/
|
*/
|
||||||
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
|
def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = {
|
||||||
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
|
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
|
||||||
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
// for getting issue count and pull request count
|
||||||
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
|
val issues = Issues.filter { t =>
|
||||||
|
t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind)
|
||||||
|
}.map(_.pullRequest).list
|
||||||
|
|
||||||
|
new RepositoryInfo(
|
||||||
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
||||||
|
repository,
|
||||||
|
issues.size,
|
||||||
|
issues.filter(_ == true).size,
|
||||||
|
getForkedCount(
|
||||||
|
repository.originUserName.getOrElse(repository.userName),
|
||||||
|
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||||
|
),
|
||||||
|
getRepositoryManagers(repository.userName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAllRepositories()(implicit s: Session): List[(String, String)] = {
|
||||||
|
Repositories.sortBy(_.lastActivityDate desc).map{ t =>
|
||||||
|
(t.userName, t.repositoryName)
|
||||||
|
}.list
|
||||||
|
}
|
||||||
|
|
||||||
|
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
|
||||||
|
(implicit s: Session): List[RepositoryInfo] = {
|
||||||
|
Repositories.filter { t1 =>
|
||||||
|
(t1.userName === userName.bind) ||
|
||||||
|
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
|
||||||
|
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||||
|
new RepositoryInfo(
|
||||||
|
if(withoutPhysicalInfo){
|
||||||
|
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||||
|
} else {
|
||||||
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||||
|
},
|
||||||
|
repository,
|
||||||
|
getForkedCount(
|
||||||
|
repository.originUserName.getOrElse(repository.userName),
|
||||||
|
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||||
|
),
|
||||||
|
getRepositoryManagers(repository.userName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of accessible repositories information for the specified account user.
|
* Returns the list of visible repositories for the specified user.
|
||||||
*
|
* If repositoryUserName is given then filters results by repository owner.
|
||||||
* @param account the account
|
*
|
||||||
|
* @param loginAccount the logged in account
|
||||||
* @param baseUrl the base url of this application
|
* @param baseUrl the base url of this application
|
||||||
* @return the repository informations which is sorted in descending order of lastActivityDate.
|
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
|
||||||
|
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
|
||||||
|
* branches and tags
|
||||||
|
* @return the repository information which is sorted in descending order of lastActivityDate.
|
||||||
*/
|
*/
|
||||||
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = {
|
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
|
||||||
|
withoutPhysicalInfo: Boolean = false)
|
||||||
def createRepositoryInfo(repository: Repository): RepositoryInfo = {
|
(implicit s: Session): List[RepositoryInfo] = {
|
||||||
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
(loginAccount match {
|
||||||
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
(account match {
|
|
||||||
// for Administrators
|
// for Administrators
|
||||||
case Some(x) if(x.isAdmin) => Query(Repositories)
|
case Some(x) if(x.isAdmin) => Repositories
|
||||||
// for Normal Users
|
// for Normal Users
|
||||||
case Some(x) if(!x.isAdmin) =>
|
case Some(x) if(!x.isAdmin) =>
|
||||||
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
|
Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) ||
|
||||||
(Query(Collaborators).filter(t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)) exists)
|
(Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists)
|
||||||
}
|
}
|
||||||
// for Guests
|
// for Guests
|
||||||
case None => Query(Repositories) filter(_.isPrivate is false.bind)
|
case None => Repositories filter(_.isPrivate === false.bind)
|
||||||
}).sortBy(_.lastActivityDate desc).list.map(createRepositoryInfo _)
|
}).filter { t =>
|
||||||
|
repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true)
|
||||||
|
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||||
|
new RepositoryInfo(
|
||||||
|
if(withoutPhysicalInfo){
|
||||||
|
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||||
|
} else {
|
||||||
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||||
|
},
|
||||||
|
repository,
|
||||||
|
getForkedCount(
|
||||||
|
repository.originUserName.getOrElse(repository.userName),
|
||||||
|
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||||
|
),
|
||||||
|
getRepositoryManagers(repository.userName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
|
||||||
|
if(getAccountByUserName(userName).exists(_.isGroupAccount)){
|
||||||
|
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
|
||||||
|
} else {
|
||||||
|
Seq(userName)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the last activity date of the repository.
|
* Updates the last activity date of the repository.
|
||||||
*/
|
*/
|
||||||
def updateLastActivityDate(userName: String, repositoryName: String): Unit =
|
def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit =
|
||||||
Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
|
Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save repository options.
|
* Save repository options.
|
||||||
*/
|
*/
|
||||||
def saveRepositoryOptions(userName: String, repositoryName: String,
|
def saveRepositoryOptions(userName: String, repositoryName: String,
|
||||||
description: Option[String], defaultBranch: String, isPrivate: Boolean): Unit =
|
description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
|
||||||
Repositories.filter(_.byRepository(userName, repositoryName))
|
Repositories.filter(_.byRepository(userName, repositoryName))
|
||||||
.map { r => r.description.? ~ r.defaultBranch ~ r.isPrivate ~ r.updatedDate }
|
.map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
|
||||||
.update (description, defaultBranch, isPrivate, currentDate)
|
.update (description, defaultBranch, isPrivate, currentDate)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,8 +264,8 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* @param repositoryName the repository name
|
* @param repositoryName the repository name
|
||||||
* @param collaboratorName the collaborator name
|
* @param collaboratorName the collaborator name
|
||||||
*/
|
*/
|
||||||
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
|
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
|
||||||
Collaborators insert(Collaborator(userName, repositoryName, collaboratorName))
|
Collaborators insert Collaborator(userName, repositoryName, collaboratorName)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove collaborator from the repository.
|
* Remove collaborator from the repository.
|
||||||
@@ -158,9 +274,18 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* @param repositoryName the repository name
|
* @param repositoryName the repository name
|
||||||
* @param collaboratorName the collaborator name
|
* @param collaboratorName the collaborator name
|
||||||
*/
|
*/
|
||||||
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
|
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
|
||||||
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
|
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all collaborators from the repository.
|
||||||
|
*
|
||||||
|
* @param userName the user name of the repository owner
|
||||||
|
* @param repositoryName the repository name
|
||||||
|
*/
|
||||||
|
def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
|
||||||
|
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of collaborators name which is sorted with ascending order.
|
* Returns the list of collaborators name which is sorted with ascending order.
|
||||||
*
|
*
|
||||||
@@ -168,10 +293,10 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* @param repositoryName the repository name
|
* @param repositoryName the repository name
|
||||||
* @return the list of collaborators name
|
* @return the list of collaborators name
|
||||||
*/
|
*/
|
||||||
def getCollaborators(userName: String, repositoryName: String): List[String] =
|
def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] =
|
||||||
Query(Collaborators).filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
|
Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
|
||||||
|
|
||||||
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account]): Boolean = {
|
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
|
||||||
loginAccount match {
|
loginAccount match {
|
||||||
case Some(a) if(a.isAdmin) => true
|
case Some(a) if(a.isAdmin) => true
|
||||||
case Some(a) if(a.userName == owner) => true
|
case Some(a) if(a.userName == owner) => true
|
||||||
@@ -180,11 +305,43 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
|
||||||
|
Query(Repositories.filter { t =>
|
||||||
|
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
|
||||||
|
}.length).first
|
||||||
|
|
||||||
|
|
||||||
|
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
|
||||||
|
Repositories.filter { t =>
|
||||||
|
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
|
||||||
|
}
|
||||||
|
.sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object RepositoryService {
|
object RepositoryService {
|
||||||
|
|
||||||
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
|
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
|
||||||
branchList: List[String], tags: List[util.JGitUtil.TagInfo])
|
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
|
||||||
|
branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){
|
||||||
|
|
||||||
|
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
|
||||||
|
|
||||||
|
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates instance with issue count and pull request count.
|
||||||
|
*/
|
||||||
|
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
|
||||||
|
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates instance without issue count and pull request count.
|
||||||
|
*/
|
||||||
|
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
|
||||||
|
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
|
||||||
|
|
||||||
}
|
}
|
||||||
37
src/main/scala/service/RequestCache.scala
Normal file
37
src/main/scala/service/RequestCache.scala
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import model.{Account, Issue, Session}
|
||||||
|
import util.Implicits.request2Session
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service is used for a view helper mainly.
|
||||||
|
*
|
||||||
|
* It may be called many times in one request, so each method stores
|
||||||
|
* its result into the cache which available during a request.
|
||||||
|
*/
|
||||||
|
trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
|
||||||
|
|
||||||
|
private implicit def context2Session(implicit context: app.Context): Session =
|
||||||
|
request2Session(context.request)
|
||||||
|
|
||||||
|
def getIssue(userName: String, repositoryName: String, issueId: String)
|
||||||
|
(implicit context: app.Context): Option[Issue] = {
|
||||||
|
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
|
||||||
|
super.getIssue(userName, repositoryName, issueId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAccountByUserName(userName: String)
|
||||||
|
(implicit context: app.Context): Option[Account] = {
|
||||||
|
context.cache(s"account.${userName}"){
|
||||||
|
super.getAccountByUserName(userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAccountByMailAddress(mailAddress: String)
|
||||||
|
(implicit context: app.Context): Option[Account] = {
|
||||||
|
context.cache(s"account.${mailAddress}"){
|
||||||
|
super.getAccountByMailAddress(mailAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/scala/service/SshKeyService.scala
Normal file
18
src/main/scala/service/SshKeyService.scala
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
|
import model.SshKey
|
||||||
|
|
||||||
|
trait SshKeyService {
|
||||||
|
|
||||||
|
def addPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit =
|
||||||
|
SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey)
|
||||||
|
|
||||||
|
def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] =
|
||||||
|
SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list
|
||||||
|
|
||||||
|
def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit =
|
||||||
|
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,40 +1,194 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
trait SystemSettingsService {
|
trait SystemSettingsService {
|
||||||
|
|
||||||
|
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request)
|
||||||
|
|
||||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||||
val props = new java.util.Properties()
|
defining(new java.util.Properties()){ props =>
|
||||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||||
props.store(new java.io.FileOutputStream(GitBucketConf), null)
|
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||||
|
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||||
|
props.setProperty(Notification, settings.notification.toString)
|
||||||
|
props.setProperty(Ssh, settings.ssh.toString)
|
||||||
|
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
||||||
|
if(settings.notification) {
|
||||||
|
settings.smtp.foreach { smtp =>
|
||||||
|
props.setProperty(SmtpHost, smtp.host)
|
||||||
|
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
||||||
|
smtp.user.foreach(props.setProperty(SmtpUser, _))
|
||||||
|
smtp.password.foreach(props.setProperty(SmtpPassword, _))
|
||||||
|
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
||||||
|
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
|
||||||
|
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
|
||||||
|
if(settings.ldapAuthentication){
|
||||||
|
settings.ldap.map { ldap =>
|
||||||
|
props.setProperty(LdapHost, ldap.host)
|
||||||
|
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
|
||||||
|
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
|
||||||
|
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
|
||||||
|
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||||
|
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||||
|
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
|
||||||
|
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
||||||
|
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x.toString))
|
||||||
|
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
||||||
|
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
|
||||||
|
props.store(out, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def loadSystemSettings(): SystemSettings = {
|
def loadSystemSettings(): SystemSettings = {
|
||||||
val props = new java.util.Properties()
|
defining(new java.util.Properties()){ props =>
|
||||||
if(GitBucketConf.exists){
|
if(GitBucketConf.exists){
|
||||||
props.load(new java.io.FileInputStream(GitBucketConf))
|
using(new java.io.FileInputStream(GitBucketConf)){ in =>
|
||||||
|
props.load(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemSettings(
|
||||||
|
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||||
|
getValue(props, AllowAccountRegistration, false),
|
||||||
|
getValue(props, Gravatar, true),
|
||||||
|
getValue(props, Notification, false),
|
||||||
|
getValue(props, Ssh, false),
|
||||||
|
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||||
|
if(getValue(props, Notification, false)){
|
||||||
|
Some(Smtp(
|
||||||
|
getValue(props, SmtpHost, ""),
|
||||||
|
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
||||||
|
getOptionValue(props, SmtpUser, None),
|
||||||
|
getOptionValue(props, SmtpPassword, None),
|
||||||
|
getOptionValue[Boolean](props, SmtpSsl, None),
|
||||||
|
getOptionValue(props, SmtpFromAddress, None),
|
||||||
|
getOptionValue(props, SmtpFromName, None)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
getValue(props, LdapAuthentication, false),
|
||||||
|
if(getValue(props, LdapAuthentication, false)){
|
||||||
|
Some(Ldap(
|
||||||
|
getValue(props, LdapHost, ""),
|
||||||
|
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
|
||||||
|
getOptionValue(props, LdapBindDN, None),
|
||||||
|
getOptionValue(props, LdapBindPassword, None),
|
||||||
|
getValue(props, LdapBaseDN, ""),
|
||||||
|
getValue(props, LdapUserNameAttribute, ""),
|
||||||
|
getOptionValue(props, LdapAdditionalFilterCondition, None),
|
||||||
|
getOptionValue(props, LdapFullNameAttribute, None),
|
||||||
|
getOptionValue(props, LdapMailAddressAttribute, None),
|
||||||
|
getOptionValue[Boolean](props, LdapTls, None),
|
||||||
|
getOptionValue(props, LdapKeystore, None)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
SystemSettings(getBoolean(props, "allow_account_registration"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object SystemSettingsService {
|
object SystemSettingsService {
|
||||||
|
import scala.reflect.ClassTag
|
||||||
|
|
||||||
case class SystemSettings(allowAccountRegistration: Boolean)
|
case class SystemSettings(
|
||||||
|
baseUrl: Option[String],
|
||||||
private val AllowAccountRegistration = "allow_account_registration"
|
allowAccountRegistration: Boolean,
|
||||||
|
gravatar: Boolean,
|
||||||
private def getBoolean(props: java.util.Properties, key: String, default: Boolean = false): Boolean = {
|
notification: Boolean,
|
||||||
val value = props.getProperty(key)
|
ssh: Boolean,
|
||||||
if(value == null || value.isEmpty){
|
sshPort: Option[Int],
|
||||||
default
|
smtp: Option[Smtp],
|
||||||
} else {
|
ldapAuthentication: Boolean,
|
||||||
value.toBoolean
|
ldap: Option[Ldap]){
|
||||||
}
|
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
|
||||||
|
defining(request.getRequestURL.toString){ url =>
|
||||||
|
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||||
|
}
|
||||||
|
}.stripSuffix("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class Ldap(
|
||||||
|
host: String,
|
||||||
|
port: Option[Int],
|
||||||
|
bindDN: Option[String],
|
||||||
|
bindPassword: Option[String],
|
||||||
|
baseDN: String,
|
||||||
|
userNameAttribute: String,
|
||||||
|
additionalFilterCondition: Option[String],
|
||||||
|
fullNameAttribute: Option[String],
|
||||||
|
mailAttribute: Option[String],
|
||||||
|
tls: Option[Boolean],
|
||||||
|
keystore: Option[String])
|
||||||
|
|
||||||
|
case class Smtp(
|
||||||
|
host: String,
|
||||||
|
port: Option[Int],
|
||||||
|
user: Option[String],
|
||||||
|
password: Option[String],
|
||||||
|
ssl: Option[Boolean],
|
||||||
|
fromAddress: Option[String],
|
||||||
|
fromName: Option[String])
|
||||||
|
|
||||||
|
val DefaultSshPort = 29418
|
||||||
|
val DefaultSmtpPort = 25
|
||||||
|
val DefaultLdapPort = 389
|
||||||
|
|
||||||
|
private val BaseURL = "base_url"
|
||||||
|
private val AllowAccountRegistration = "allow_account_registration"
|
||||||
|
private val Gravatar = "gravatar"
|
||||||
|
private val Notification = "notification"
|
||||||
|
private val Ssh = "ssh"
|
||||||
|
private val SshPort = "ssh.port"
|
||||||
|
private val SmtpHost = "smtp.host"
|
||||||
|
private val SmtpPort = "smtp.port"
|
||||||
|
private val SmtpUser = "smtp.user"
|
||||||
|
private val SmtpPassword = "smtp.password"
|
||||||
|
private val SmtpSsl = "smtp.ssl"
|
||||||
|
private val SmtpFromAddress = "smtp.from_address"
|
||||||
|
private val SmtpFromName = "smtp.from_name"
|
||||||
|
private val LdapAuthentication = "ldap_authentication"
|
||||||
|
private val LdapHost = "ldap.host"
|
||||||
|
private val LdapPort = "ldap.port"
|
||||||
|
private val LdapBindDN = "ldap.bindDN"
|
||||||
|
private val LdapBindPassword = "ldap.bind_password"
|
||||||
|
private val LdapBaseDN = "ldap.baseDN"
|
||||||
|
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||||
|
private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
|
||||||
|
private val LdapFullNameAttribute = "ldap.fullname_attribute"
|
||||||
|
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||||
|
private val LdapTls = "ldap.tls"
|
||||||
|
private val LdapKeystore = "ldap.keystore"
|
||||||
|
|
||||||
|
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
||||||
|
defining(props.getProperty(key)){ value =>
|
||||||
|
if(value == null || value.isEmpty) default
|
||||||
|
else convertType(value).asInstanceOf[A]
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
|
||||||
|
defining(props.getProperty(key)){ value =>
|
||||||
|
if(value == null || value.isEmpty) default
|
||||||
|
else Some(convertType(value)).asInstanceOf[Option[A]]
|
||||||
|
}
|
||||||
|
|
||||||
|
private def convertType[A: ClassTag](value: String) =
|
||||||
|
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
|
||||||
|
if(c == classOf[Boolean]) value.toBoolean
|
||||||
|
else if(c == classOf[Int]) value.toInt
|
||||||
|
else value
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
143
src/main/scala/service/WebHookService.scala
Normal file
143
src/main/scala/service/WebHookService.scala
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
|
import model.{WebHook, Account}
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import service.RepositoryService.RepositoryInfo
|
||||||
|
import util.JGitUtil
|
||||||
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.apache.http.message.BasicNameValuePair
|
||||||
|
import org.apache.http.client.entity.UrlEncodedFormEntity
|
||||||
|
import org.apache.http.NameValuePair
|
||||||
|
|
||||||
|
trait WebHookService {
|
||||||
|
import WebHookService._
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
|
||||||
|
|
||||||
|
def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] =
|
||||||
|
WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list
|
||||||
|
|
||||||
|
def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
|
||||||
|
WebHooks insert WebHook(owner, repository, url)
|
||||||
|
|
||||||
|
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
|
||||||
|
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
|
||||||
|
|
||||||
|
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
|
||||||
|
import org.json4s._
|
||||||
|
import org.json4s.jackson.Serialization
|
||||||
|
import org.json4s.jackson.Serialization.{read, write}
|
||||||
|
import org.apache.http.client.methods.HttpPost
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder
|
||||||
|
import scala.concurrent._
|
||||||
|
import ExecutionContext.Implicits.global
|
||||||
|
|
||||||
|
logger.debug("start callWebHook")
|
||||||
|
implicit val formats = Serialization.formats(NoTypeHints)
|
||||||
|
|
||||||
|
if(webHookURLs.nonEmpty){
|
||||||
|
val json = write(payload)
|
||||||
|
val httpClient = HttpClientBuilder.create.build
|
||||||
|
|
||||||
|
webHookURLs.foreach { webHookUrl =>
|
||||||
|
val f = Future {
|
||||||
|
logger.debug(s"start web hook invocation for ${webHookUrl}")
|
||||||
|
val httpPost = new HttpPost(webHookUrl.url)
|
||||||
|
|
||||||
|
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||||
|
params.add(new BasicNameValuePair("payload", json))
|
||||||
|
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
||||||
|
|
||||||
|
httpClient.execute(httpPost)
|
||||||
|
httpPost.releaseConnection()
|
||||||
|
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
||||||
|
}
|
||||||
|
f.onSuccess {
|
||||||
|
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
||||||
|
}
|
||||||
|
f.onFailure {
|
||||||
|
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("end callWebHook")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object WebHookService {
|
||||||
|
|
||||||
|
case class WebHookPayload(
|
||||||
|
pusher: WebHookUser,
|
||||||
|
ref: String,
|
||||||
|
commits: List[WebHookCommit],
|
||||||
|
repository: WebHookRepository)
|
||||||
|
|
||||||
|
object WebHookPayload {
|
||||||
|
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
|
||||||
|
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
|
||||||
|
WebHookPayload(
|
||||||
|
WebHookUser(pusher.fullName, pusher.mailAddress),
|
||||||
|
refName,
|
||||||
|
commits.map { commit =>
|
||||||
|
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||||
|
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id
|
||||||
|
|
||||||
|
WebHookCommit(
|
||||||
|
id = commit.id,
|
||||||
|
message = commit.fullMessage,
|
||||||
|
timestamp = commit.commitTime.toString,
|
||||||
|
url = commitUrl,
|
||||||
|
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
|
||||||
|
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
|
||||||
|
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
|
||||||
|
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
|
||||||
|
author = WebHookUser(
|
||||||
|
name = commit.committerName,
|
||||||
|
email = commit.committerEmailAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
WebHookRepository(
|
||||||
|
name = repositoryInfo.name,
|
||||||
|
url = repositoryInfo.httpUrl,
|
||||||
|
description = repositoryInfo.repository.description.getOrElse(""),
|
||||||
|
watchers = 0,
|
||||||
|
forks = repositoryInfo.forkedCount,
|
||||||
|
`private` = repositoryInfo.repository.isPrivate,
|
||||||
|
owner = WebHookUser(
|
||||||
|
name = repositoryOwner.userName,
|
||||||
|
email = repositoryOwner.mailAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class WebHookCommit(
|
||||||
|
id: String,
|
||||||
|
message: String,
|
||||||
|
timestamp: String,
|
||||||
|
url: String,
|
||||||
|
added: List[String],
|
||||||
|
removed: List[String],
|
||||||
|
modified: List[String],
|
||||||
|
author: WebHookUser)
|
||||||
|
|
||||||
|
case class WebHookRepository(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
description: String,
|
||||||
|
watchers: Int,
|
||||||
|
forks: Int,
|
||||||
|
`private`: Boolean,
|
||||||
|
owner: WebHookUser)
|
||||||
|
|
||||||
|
case class WebHookUser(
|
||||||
|
name: String,
|
||||||
|
email: String)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,224 +1,278 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import java.io.File
|
import java.util.Date
|
||||||
import java.util.Date
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.api.Git
|
import util._
|
||||||
import org.apache.commons.io.FileUtils
|
import _root_.util.ControlUtil._
|
||||||
import util.JGitUtil.DiffInfo
|
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
||||||
import util.{Directory, JGitUtil}
|
import org.eclipse.jgit.lib._
|
||||||
import org.eclipse.jgit.lib.RepositoryBuilder
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.io.ByteArrayInputStream
|
||||||
|
import org.eclipse.jgit.patch._
|
||||||
object WikiService {
|
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
/**
|
import service.RepositoryService.RepositoryInfo
|
||||||
* The model for wiki page.
|
|
||||||
*
|
object WikiService {
|
||||||
* @param name the page name
|
|
||||||
* @param content the page content
|
/**
|
||||||
* @param committer the last committer
|
* The model for wiki page.
|
||||||
* @param time the last modified time
|
*
|
||||||
*/
|
* @param name the page name
|
||||||
case class WikiPageInfo(name: String, content: String, committer: String, time: Date)
|
* @param content the page content
|
||||||
|
* @param committer the last committer
|
||||||
/**
|
* @param time the last modified time
|
||||||
* The model for wiki page history.
|
* @param id the latest commit id
|
||||||
*
|
*/
|
||||||
* @param name the page name
|
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
|
||||||
* @param committer the committer the committer
|
|
||||||
* @param message the commit message
|
/**
|
||||||
* @param date the commit date
|
* The model for wiki page history.
|
||||||
*/
|
*
|
||||||
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
* @param name the page name
|
||||||
|
* @param committer the committer the committer
|
||||||
/**
|
* @param message the commit message
|
||||||
* lock objects
|
* @param date the commit date
|
||||||
*/
|
*/
|
||||||
private val locks = new ConcurrentHashMap[String, AnyRef]()
|
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
||||||
|
|
||||||
/**
|
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
|
||||||
* Returns the lock object for the specified repository.
|
|
||||||
*/
|
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
|
||||||
private def getLockObject(owner: String, repository: String): AnyRef = synchronized {
|
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
|
||||||
val key = owner + "/" + repository
|
}
|
||||||
if(!locks.containsKey(key)){
|
|
||||||
locks.put(key, new AnyRef())
|
trait WikiService {
|
||||||
}
|
import WikiService._
|
||||||
locks.get(key)
|
|
||||||
}
|
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
|
||||||
|
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||||
/**
|
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
|
||||||
* Synchronizes a given function which modifies the working copy of the wiki repository.
|
if(!dir.exists){
|
||||||
*
|
JGitUtil.initRepository(dir)
|
||||||
* @param owner the repository owner
|
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
|
||||||
* @param repository the repository name
|
}
|
||||||
* @param f the function which modifies the working copy of the wiki repository
|
}
|
||||||
* @tparam T the return type of the given function
|
}
|
||||||
* @return the result of the given function
|
|
||||||
*/
|
/**
|
||||||
def lock[T](owner: String, repository: String)(f: => T): T = getLockObject(owner, repository).synchronized(f)
|
* Returns the wiki page.
|
||||||
|
*/
|
||||||
}
|
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
trait WikiService {
|
if(!JGitUtil.isEmpty(git)){
|
||||||
import WikiService._
|
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
||||||
|
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
|
||||||
def createWikiRepository(owner: model.Account, repository: String): Unit = {
|
file.author, file.time, file.commitId)
|
||||||
lock(owner.userName, repository){
|
}
|
||||||
val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
|
} else None
|
||||||
if(!dir.exists){
|
}
|
||||||
val repo = new RepositoryBuilder().setGitDir(dir).setBare.build
|
}
|
||||||
try {
|
|
||||||
repo.create
|
/**
|
||||||
saveWikiPage(owner.userName, repository, "Home", "Home", "Welcome to the %s wiki!!".format(repository), owner, "Initial Commit")
|
* Returns the content of the specified file.
|
||||||
} finally {
|
*/
|
||||||
repo.close
|
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
|
||||||
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository))
|
if(!JGitUtil.isEmpty(git)){
|
||||||
}
|
val index = path.lastIndexOf('/')
|
||||||
}
|
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||||
}
|
val fileName = if(index < 0) path else path.substring(index + 1)
|
||||||
}
|
|
||||||
|
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
||||||
/**
|
git.getRepository.open(file.id).getBytes
|
||||||
* Returns the wiki page.
|
}
|
||||||
*/
|
} else None
|
||||||
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
}
|
||||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
|
||||||
try {
|
/**
|
||||||
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
* Returns the list of wiki page names.
|
||||||
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
|
*/
|
||||||
}
|
def getWikiPageList(owner: String, repository: String): List[String] = {
|
||||||
} catch {
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
// TODO no commit, but it should not judge by exception.
|
JGitUtil.getFileList(git, "master", ".")
|
||||||
case e: NullPointerException => None
|
.filter(_.name.endsWith(".md"))
|
||||||
}
|
.map(_.name.stripSuffix(".md"))
|
||||||
}
|
.sortBy(x => x)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Returns the content of the specified file.
|
/**
|
||||||
*/
|
* Reverts specified changes.
|
||||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = {
|
*/
|
||||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
def revertWikiPage(owner: String, repository: String, from: String, to: String,
|
||||||
try {
|
committer: model.Account, pageName: Option[String]): Boolean = {
|
||||||
val index = path.lastIndexOf('/')
|
|
||||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
case class RevertInfo(operation: String, filePath: String, source: String)
|
||||||
val fileName = if(index < 0) path else path.substring(index + 1)
|
|
||||||
|
try {
|
||||||
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||||
git.getRepository.open(file.id).getBytes
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
}
|
|
||||||
} catch {
|
val reader = git.getRepository.newObjectReader
|
||||||
// TODO no commit, but it should not judge by exception.
|
val oldTreeIter = new CanonicalTreeParser
|
||||||
case e: NullPointerException => None
|
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
|
||||||
}
|
|
||||||
}
|
val newTreeIter = new CanonicalTreeParser
|
||||||
}
|
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||||
|
|
||||||
/**
|
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
|
||||||
* Returns the list of wiki page names.
|
pageName match {
|
||||||
*/
|
case Some(x) => diff.getNewPath == x + ".md"
|
||||||
def getWikiPageList(owner: String, repository: String): List[String] = {
|
case None => true
|
||||||
JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
|
}
|
||||||
.filter(_.name.endsWith(".md"))
|
}
|
||||||
.map(_.name.replaceFirst("\\.md$", ""))
|
|
||||||
.sortBy(x => x)
|
val patch = using(new java.io.ByteArrayOutputStream()){ out =>
|
||||||
}
|
val formatter = new DiffFormatter(out)
|
||||||
|
formatter.setRepository(git.getRepository)
|
||||||
/**
|
formatter.format(diffs.asJava)
|
||||||
* Save the wiki page.
|
new String(out.toByteArray, "UTF-8")
|
||||||
*/
|
}
|
||||||
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
|
||||||
content: String, committer: model.Account, message: String): Unit = {
|
val p = new Patch()
|
||||||
|
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
|
||||||
lock(owner, repository){
|
if(!p.getErrors.isEmpty){
|
||||||
// clone working copy
|
throw new PatchFormatException(p.getErrors())
|
||||||
val workDir = Directory.getWikiWorkDir(owner, repository)
|
}
|
||||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
val revertInfo = (p.getFiles.asScala.map { fh =>
|
||||||
|
fh.getChangeType match {
|
||||||
// write as file
|
case DiffEntry.ChangeType.MODIFY => {
|
||||||
JGitUtil.withGit(workDir){ git =>
|
val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("")
|
||||||
val file = new File(workDir, newPageName + ".md")
|
val applied = PatchUtil.apply(source, patch, fh)
|
||||||
val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
|
if(applied != null){
|
||||||
FileUtils.writeStringToFile(file, content, "UTF-8")
|
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||||
git.add.addFilepattern(file.getName).call
|
} else Nil
|
||||||
true
|
}
|
||||||
} else {
|
case DiffEntry.ChangeType.ADD => {
|
||||||
false
|
val applied = PatchUtil.apply("", patch, fh)
|
||||||
}
|
if(applied != null){
|
||||||
|
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||||
// delete file
|
} else Nil
|
||||||
val deleted = if(currentPageName != "" && currentPageName != newPageName){
|
}
|
||||||
git.rm.addFilepattern(currentPageName + ".md").call
|
case DiffEntry.ChangeType.DELETE => {
|
||||||
true
|
Seq(RevertInfo("DELETE", fh.getNewPath, ""))
|
||||||
} else {
|
}
|
||||||
false
|
case DiffEntry.ChangeType.RENAME => {
|
||||||
}
|
val applied = PatchUtil.apply("", patch, fh)
|
||||||
|
if(applied != null){
|
||||||
// commit and push
|
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
|
||||||
if(added || deleted){
|
} else {
|
||||||
git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
|
Seq(RevertInfo("DELETE", fh.getOldPath, ""))
|
||||||
git.push.call
|
}
|
||||||
}
|
}
|
||||||
}
|
case _ => Nil
|
||||||
}
|
}
|
||||||
}
|
}).flatten
|
||||||
|
|
||||||
/**
|
if(revertInfo.nonEmpty){
|
||||||
* Delete the wiki page.
|
val builder = DirCache.newInCore.builder()
|
||||||
*/
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = {
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
lock(owner, repository){
|
|
||||||
// clone working copy
|
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||||
val workDir = Directory.getWikiWorkDir(owner, repository)
|
if(revertInfo.find(x => x.filePath == path).isEmpty){
|
||||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
|
}
|
||||||
// delete file
|
}
|
||||||
new File(workDir, pageName + ".md").delete
|
|
||||||
|
revertInfo.filter(_.operation == "ADD").foreach { x =>
|
||||||
JGitUtil.withGit(workDir){ git =>
|
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
|
||||||
git.rm.addFilepattern(pageName + ".md").call
|
}
|
||||||
|
builder.finish()
|
||||||
// commit and push
|
|
||||||
// TODO committer's mail address
|
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||||
git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call
|
pageName match {
|
||||||
git.push.call
|
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
|
||||||
}
|
case None => s"Revert ${from} ... ${to}"
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
}
|
||||||
* Returns differences between specified commits.
|
true
|
||||||
*/
|
} catch {
|
||||||
def getWikiDiffs(git: Git, commitId1: String, commitId2: String): List[DiffInfo] = {
|
case e: Exception => {
|
||||||
// get diff between specified commit and its previous commit
|
e.printStackTrace()
|
||||||
val reader = git.getRepository.newObjectReader
|
false
|
||||||
|
}
|
||||||
val oldTreeIter = new CanonicalTreeParser
|
}
|
||||||
oldTreeIter.reset(reader, git.getRepository.resolve(commitId1 + "^{tree}"))
|
}
|
||||||
|
|
||||||
val newTreeIter = new CanonicalTreeParser
|
/**
|
||||||
newTreeIter.reset(reader, git.getRepository.resolve(commitId2 + "^{tree}"))
|
* Save the wiki page.
|
||||||
|
*/
|
||||||
import scala.collection.JavaConverters._
|
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
||||||
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
|
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
|
||||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||||
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).map(new String(_, "UTF-8")),
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).map(new String(_, "UTF-8")))
|
val builder = DirCache.newInCore.builder()
|
||||||
}.toList
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
}
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
var created = true
|
||||||
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
|
var updated = false
|
||||||
if(!workDir.exists){
|
var removed = false
|
||||||
Git.cloneRepository
|
|
||||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
if(headId != null){
|
||||||
.setDirectory(workDir)
|
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||||
.call
|
if(path == currentPageName + ".md" && currentPageName != newPageName){
|
||||||
} else {
|
removed = true
|
||||||
Git.open(workDir).pull.call
|
} else if(path != newPageName + ".md"){
|
||||||
}
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
}
|
} else {
|
||||||
|
created = false
|
||||||
}
|
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(created || updated || removed){
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||||
|
builder.finish()
|
||||||
|
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||||
|
if(message.trim.length == 0) {
|
||||||
|
if(removed){
|
||||||
|
s"Rename ${currentPageName} to ${newPageName}"
|
||||||
|
} else if(created){
|
||||||
|
s"Created ${newPageName}"
|
||||||
|
} else {
|
||||||
|
s"Updated ${newPageName}"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
})
|
||||||
|
|
||||||
|
Some(newHeadId.getName)
|
||||||
|
} else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the wiki page.
|
||||||
|
*/
|
||||||
|
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||||
|
committer: String, mailAddress: String, message: String): Unit = {
|
||||||
|
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
|
val builder = DirCache.newInCore.builder()
|
||||||
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
var removed = false
|
||||||
|
|
||||||
|
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||||
|
if(path != pageName + ".md"){
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
|
} else {
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(removed){
|
||||||
|
builder.finish()
|
||||||
|
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,118 +1,211 @@
|
|||||||
package servlet
|
package servlet
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.sql.Connection
|
import java.sql.{DriverManager, Connection}
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import javax.servlet.ServletContextEvent
|
import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import util.Directory
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
object AutoUpdate {
|
import org.eclipse.jgit.api.Git
|
||||||
|
import util.Directory
|
||||||
/**
|
import plugin.PluginUpdateJob
|
||||||
* Version of GitBucket
|
|
||||||
*
|
object AutoUpdate {
|
||||||
* @param majorVersion the major version
|
|
||||||
|
/**
|
||||||
|
* Version of GitBucket
|
||||||
|
*
|
||||||
|
* @param majorVersion the major version
|
||||||
* @param minorVersion the minor version
|
* @param minorVersion the minor version
|
||||||
*/
|
*/
|
||||||
case class Version(majorVersion: Int, minorVersion: Int){
|
case class Version(majorVersion: Int, minorVersion: Int){
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
|
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute update/MAJOR_MINOR.sql to update schema to this version.
|
* Execute update/MAJOR_MINOR.sql to update schema to this version.
|
||||||
* If corresponding SQL file does not exist, this method do nothing.
|
* If corresponding SQL file does not exist, this method do nothing.
|
||||||
*/
|
*/
|
||||||
def update(conn: Connection): Unit = {
|
def update(conn: Connection): Unit = {
|
||||||
val sqlPath = "update/%d_%d.sql".format(majorVersion, minorVersion)
|
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
|
||||||
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
|
|
||||||
if(in != null){
|
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
|
||||||
val sql = IOUtils.toString(in, "UTF-8")
|
if(in != null){
|
||||||
val stmt = conn.createStatement()
|
val sql = IOUtils.toString(in, "UTF-8")
|
||||||
try {
|
using(conn.createStatement()){ stmt =>
|
||||||
logger.debug(sqlPath + "=" + sql)
|
logger.debug(sqlPath + "=" + sql)
|
||||||
stmt.executeUpdate(sql)
|
stmt.executeUpdate(sql)
|
||||||
} finally {
|
}
|
||||||
stmt.close()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
|
||||||
* MAJOR.MINOR
|
* MAJOR.MINOR
|
||||||
*/
|
*/
|
||||||
val versionString = "%d.%d".format(majorVersion, minorVersion)
|
val versionString = s"${majorVersion}.${minorVersion}"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||||
*/
|
*/
|
||||||
val versions = Seq(
|
val versions = Seq(
|
||||||
Version(1, 0)
|
new Version(2, 2),
|
||||||
)
|
new Version(2, 1),
|
||||||
|
new Version(2, 0){
|
||||||
/**
|
override def update(conn: Connection): Unit = {
|
||||||
* The head version of BitBucket.
|
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
|
||||||
*/
|
|
||||||
val headVersion = versions.head
|
val mimeUtil = new MimeUtil2()
|
||||||
|
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
||||||
/**
|
|
||||||
* The version file (GITBUCKET_HOME/version).
|
super.update(conn)
|
||||||
*/
|
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
||||||
val versionFile = new File(Directory.GitBucketHome, "version")
|
while(rs.next){
|
||||||
|
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
||||||
/**
|
if(dir.exists && dir.isDirectory){
|
||||||
* Returns the current version from the version file.
|
dir.listFiles.foreach { file =>
|
||||||
*/
|
if(file.getName.indexOf('.') < 0){
|
||||||
def getCurrentVersion(): Version = {
|
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
|
||||||
if(versionFile.exists){
|
if(mimeType.startsWith("image/")){
|
||||||
FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match {
|
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
|
||||||
case Array(majorVersion, minorVersion) => {
|
}
|
||||||
versions.find { v =>
|
}
|
||||||
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
|
}
|
||||||
}.getOrElse(Version(0, 0))
|
}
|
||||||
}
|
}
|
||||||
case _ => Version(0, 0)
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Version(0, 0)
|
},
|
||||||
}
|
Version(1, 13),
|
||||||
|
Version(1, 12),
|
||||||
}
|
Version(1, 11),
|
||||||
|
Version(1, 10),
|
||||||
}
|
Version(1, 9),
|
||||||
|
Version(1, 8),
|
||||||
/**
|
Version(1, 7),
|
||||||
* Start H2 database and update schema automatically.
|
Version(1, 6),
|
||||||
*/
|
Version(1, 5),
|
||||||
class AutoUpdateListener extends org.h2.server.web.DbStarter {
|
Version(1, 4),
|
||||||
import AutoUpdate._
|
new Version(1, 3){
|
||||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
override def update(conn: Connection): Unit = {
|
||||||
|
super.update(conn)
|
||||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
// Fix wiki repository configuration
|
||||||
super.contextInitialized(event)
|
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
||||||
logger.debug("H2 started")
|
while(rs.next){
|
||||||
|
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
||||||
logger.debug("Start schema update")
|
defining(git.getRepository.getConfig){ config =>
|
||||||
val conn = getConnection()
|
if(!config.getBoolean("http", "receivepack", false)){
|
||||||
try {
|
config.setBoolean("http", null, "receivepack", true)
|
||||||
val currentVersion = getCurrentVersion()
|
config.save
|
||||||
if(currentVersion == headVersion){
|
}
|
||||||
logger.debug("No update")
|
}
|
||||||
} else {
|
}
|
||||||
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
|
}
|
||||||
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
|
}
|
||||||
conn.commit()
|
}
|
||||||
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString)
|
},
|
||||||
}
|
Version(1, 2),
|
||||||
} catch {
|
Version(1, 1),
|
||||||
case ex: Throwable => {
|
Version(1, 0),
|
||||||
logger.error("Failed to schema update", ex)
|
Version(0, 0)
|
||||||
conn.rollback()
|
)
|
||||||
}
|
|
||||||
}
|
/**
|
||||||
logger.debug("End schema update")
|
* The head version of BitBucket.
|
||||||
}
|
*/
|
||||||
|
val headVersion = versions.head
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* The version file (GITBUCKET_HOME/version).
|
||||||
|
*/
|
||||||
|
lazy val versionFile = new File(GitBucketHome, "version")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current version from the version file.
|
||||||
|
*/
|
||||||
|
def getCurrentVersion(): Version = {
|
||||||
|
if(versionFile.exists){
|
||||||
|
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
|
||||||
|
case Array(majorVersion, minorVersion) => {
|
||||||
|
versions.find { v =>
|
||||||
|
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
|
||||||
|
}.getOrElse(Version(0, 0))
|
||||||
|
}
|
||||||
|
case _ => Version(0, 0)
|
||||||
|
}
|
||||||
|
} else Version(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update database schema automatically in the context initializing.
|
||||||
|
*/
|
||||||
|
class AutoUpdateListener extends ServletContextListener {
|
||||||
|
import org.quartz.impl.StdSchedulerFactory
|
||||||
|
import org.quartz.JobBuilder._
|
||||||
|
import org.quartz.TriggerBuilder._
|
||||||
|
import org.quartz.SimpleScheduleBuilder._
|
||||||
|
import AutoUpdate._
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||||
|
private val scheduler = StdSchedulerFactory.getDefaultScheduler
|
||||||
|
|
||||||
|
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||||
|
val datadir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||||
|
if(datadir != null){
|
||||||
|
System.setProperty("gitbucket.home", datadir)
|
||||||
|
}
|
||||||
|
org.h2.Driver.load()
|
||||||
|
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
|
||||||
|
|
||||||
|
logger.debug("Start schema update")
|
||||||
|
defining(getConnection(event.getServletContext)){ conn =>
|
||||||
|
try {
|
||||||
|
defining(getCurrentVersion()){ currentVersion =>
|
||||||
|
if(currentVersion == headVersion){
|
||||||
|
logger.debug("No update")
|
||||||
|
} else if(!versions.contains(currentVersion)){
|
||||||
|
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
|
||||||
|
} else {
|
||||||
|
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
|
||||||
|
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
|
||||||
|
conn.commit()
|
||||||
|
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case ex: Throwable => {
|
||||||
|
logger.error("Failed to schema update", ex)
|
||||||
|
ex.printStackTrace()
|
||||||
|
conn.rollback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("End schema update")
|
||||||
|
|
||||||
|
logger.debug("Starting plugin system...")
|
||||||
|
plugin.PluginSystem.init()
|
||||||
|
|
||||||
|
scheduler.start()
|
||||||
|
PluginUpdateJob.schedule(scheduler)
|
||||||
|
logger.debug("PluginUpdateJob is started.")
|
||||||
|
|
||||||
|
logger.debug("Plugin system is initialized.")
|
||||||
|
}
|
||||||
|
|
||||||
|
def contextDestroyed(sce: ServletContextEvent): Unit = {
|
||||||
|
scheduler.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getConnection(servletContext: ServletContext): Connection =
|
||||||
|
DriverManager.getConnection(
|
||||||
|
servletContext.getInitParameter("db.url"),
|
||||||
|
servletContext.getInitParameter("db.user"),
|
||||||
|
servletContext.getInitParameter("db.password"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ package servlet
|
|||||||
|
|
||||||
import javax.servlet._
|
import javax.servlet._
|
||||||
import javax.servlet.http._
|
import javax.servlet.http._
|
||||||
import util.StringUtil._
|
import service.{SystemSettingsService, AccountService, RepositoryService}
|
||||||
import service.{AccountService, RepositoryService}
|
import model._
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import util.Implicits._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Keys
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
|
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
|
||||||
*/
|
*/
|
||||||
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService {
|
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
|
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
|
||||||
|
|
||||||
@@ -18,32 +21,41 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
def destroy(): Unit = {}
|
def destroy(): Unit = {}
|
||||||
|
|
||||||
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
||||||
val request = req.asInstanceOf[HttpServletRequest]
|
implicit val request = req.asInstanceOf[HttpServletRequest]
|
||||||
val response = res.asInstanceOf[HttpServletResponse]
|
val response = res.asInstanceOf[HttpServletResponse]
|
||||||
|
|
||||||
try {
|
val wrappedResponse = new HttpServletResponseWrapper(response){
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
override def setCharacterEncoding(encoding: String) = {}
|
||||||
val repositoryOwner = paths(2)
|
}
|
||||||
val repositoryName = paths(3).replaceFirst("\\.git$", "")
|
|
||||||
|
|
||||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
|
try {
|
||||||
case Some(repository) => {
|
defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
|
||||||
if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){
|
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
||||||
chain.doFilter(req, res)
|
case Some(repository) => {
|
||||||
} else {
|
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
|
||||||
request.getHeader("Authorization") match {
|
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
|
||||||
case null => requireAuth(response)
|
chain.doFilter(req, wrappedResponse)
|
||||||
case auth => decodeAuthHeader(auth).split(":") match {
|
} else {
|
||||||
case Array(username, password) if(isWritableUser(username, password, repository)) => {
|
request.getHeader("Authorization") match {
|
||||||
request.setAttribute("USER_NAME", username)
|
case null => requireAuth(response)
|
||||||
chain.doFilter(req, res)
|
case auth => decodeAuthHeader(auth).split(":") match {
|
||||||
|
case Array(username, password) => getWritableUser(username, password, repository) match {
|
||||||
|
case Some(account) => {
|
||||||
|
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||||
|
chain.doFilter(req, wrappedResponse)
|
||||||
|
}
|
||||||
|
case None => requireAuth(response)
|
||||||
|
}
|
||||||
|
case _ => requireAuth(response)
|
||||||
}
|
}
|
||||||
case _ => requireAuth(response)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case None => {
|
||||||
|
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
|
||||||
|
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case None => response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
case ex: Exception => {
|
case ex: Exception => {
|
||||||
@@ -53,12 +65,13 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = {
|
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
|
||||||
getAccountByUserName(username).map { account =>
|
(implicit session: Session): Option[Account] =
|
||||||
account.password == encrypt(password) && hasWritePermission(repository.owner, repository.name, Some(account))
|
authenticate(loadSystemSettings(), username, password) match {
|
||||||
} getOrElse false
|
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
|
||||||
}
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
private def requireAuth(response: HttpServletResponse): Unit = {
|
private def requireAuth(response: HttpServletResponse): Unit = {
|
||||||
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
|
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ import org.slf4j.LoggerFactory
|
|||||||
|
|
||||||
import javax.servlet.ServletConfig
|
import javax.servlet.ServletConfig
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
import util.{JGitUtil, Directory}
|
import util.{StringUtil, Keys, JGitUtil, Directory}
|
||||||
|
import util.ControlUtil._
|
||||||
|
import util.Implicits._
|
||||||
import service._
|
import service._
|
||||||
|
import WebHookService._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
import service.IssuesService.IssueSearchCondition
|
||||||
|
import model.Session
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides Git repository via HTTP.
|
* Provides Git repository via HTTP.
|
||||||
@@ -18,13 +25,13 @@ import service._
|
|||||||
* This servlet provides only Git repository functionality.
|
* This servlet provides only Git repository functionality.
|
||||||
* Authentication is provided by [[servlet.BasicAuthenticationFilter]].
|
* Authentication is provided by [[servlet.BasicAuthenticationFilter]].
|
||||||
*/
|
*/
|
||||||
class GitRepositoryServlet extends GitServlet {
|
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
|
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
|
||||||
|
|
||||||
override def init(config: ServletConfig): Unit = {
|
override def init(config: ServletConfig): Unit = {
|
||||||
setReceivePackFactory(new GitBucketReceivePackFactory())
|
setReceivePackFactory(new GitBucketReceivePackFactory())
|
||||||
|
|
||||||
// TODO are there any other ways...?
|
// TODO are there any other ways...?
|
||||||
super.init(new ServletConfig(){
|
super.init(new ServletConfig(){
|
||||||
def getInitParameter(name: String): String = name match {
|
def getInitParameter(name: String): String = name match {
|
||||||
@@ -33,59 +40,197 @@ class GitRepositoryServlet extends GitServlet {
|
|||||||
case name => config.getInitParameter(name)
|
case name => config.getInitParameter(name)
|
||||||
}
|
}
|
||||||
def getInitParameterNames(): java.util.Enumeration[String] = {
|
def getInitParameterNames(): java.util.Enumeration[String] = {
|
||||||
config.getInitParameterNames
|
config.getInitParameterNames
|
||||||
}
|
}
|
||||||
|
|
||||||
def getServletContext(): ServletContext = config.getServletContext
|
def getServletContext(): ServletContext = config.getServletContext
|
||||||
def getServletName(): String = config.getServletName
|
def getServletName(): String = config.getServletName
|
||||||
});
|
})
|
||||||
|
|
||||||
|
super.init(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = {
|
||||||
|
val agent = req.getHeader("USER-AGENT")
|
||||||
|
val index = req.getRequestURI.indexOf(".git")
|
||||||
|
if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){
|
||||||
|
// redirect for browsers
|
||||||
|
val paths = req.getRequestURI.substring(0, index).split("/")
|
||||||
|
res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last)
|
||||||
|
} else {
|
||||||
|
// response for git client
|
||||||
|
super.service(req, res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] {
|
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
|
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
|
||||||
|
|
||||||
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
||||||
val receivePack = new ReceivePack(db)
|
val receivePack = new ReceivePack(db)
|
||||||
val userName = request.getAttribute("USER_NAME")
|
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
|
||||||
|
|
||||||
logger.debug("requestURI: " + request.getRequestURI)
|
logger.debug("requestURI: " + request.getRequestURI)
|
||||||
logger.debug("userName:" + userName)
|
logger.debug("pusher:" + pusher)
|
||||||
|
|
||||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
defining(request.paths){ paths =>
|
||||||
val owner = paths(2)
|
val owner = paths(1)
|
||||||
val repository = paths(3).replaceFirst("\\.git$", "")
|
val repository = paths(2).stripSuffix(".git")
|
||||||
|
|
||||||
logger.debug("repository:" + owner + "/" + repository)
|
|
||||||
|
|
||||||
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository))
|
logger.debug("repository:" + owner + "/" + repository)
|
||||||
receivePack
|
|
||||||
|
if(!repository.endsWith(".wiki")){
|
||||||
|
defining(request) { implicit r =>
|
||||||
|
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
|
||||||
|
receivePack.setPreReceiveHook(hook)
|
||||||
|
receivePack.setPostReceiveHook(hook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receivePack
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
class CommitLogHook(owner: String, repository: String) extends PostReceiveHook
|
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
|
||||||
with RepositoryService with AccountService with IssuesService {
|
extends PostReceiveHook with PreReceiveHook
|
||||||
|
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||||
|
private var existIds: Seq[String] = Nil
|
||||||
|
|
||||||
|
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||||
|
try {
|
||||||
|
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||||
|
existIds = JGitUtil.getAllCommitIds(git)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case ex: Exception => {
|
||||||
|
logger.error(ex.toString, ex)
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||||
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git =>
|
try {
|
||||||
commands.asScala.foreach { command =>
|
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||||
JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name).foreach { commit =>
|
val pushedIds = scala.collection.mutable.Set[String]()
|
||||||
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
|
commands.asScala.foreach { command =>
|
||||||
val issueId = matchData.group(2)
|
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
|
||||||
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
|
val refName = command.getRefName.split("/")
|
||||||
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, None)
|
val branchName = refName.drop(2).mkString("/")
|
||||||
|
val commits = if (refName(1) == "tags") {
|
||||||
|
Nil
|
||||||
|
} else {
|
||||||
|
command.getType match {
|
||||||
|
case ReceiveCommand.Type.DELETE => Nil
|
||||||
|
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve all issue count in the repository
|
||||||
|
val issueCount =
|
||||||
|
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
|
||||||
|
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
|
||||||
|
|
||||||
|
// Extract new commit and apply issue comment
|
||||||
|
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
|
||||||
|
val newCommits = commits.flatMap { commit =>
|
||||||
|
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
|
||||||
|
if (issueCount > 0) {
|
||||||
|
pushedIds.add(commit.id)
|
||||||
|
createIssueComment(commit)
|
||||||
|
// close issues
|
||||||
|
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
|
||||||
|
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(commit)
|
||||||
|
} else None
|
||||||
|
}
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
if(refName(1) == "heads"){
|
||||||
|
command.getType match {
|
||||||
|
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
|
||||||
|
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
|
||||||
|
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
} else if(refName(1) == "tags"){
|
||||||
|
command.getType match {
|
||||||
|
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||||
|
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(refName(1) == "heads"){
|
||||||
|
command.getType match {
|
||||||
|
case ReceiveCommand.Type.CREATE |
|
||||||
|
ReceiveCommand.Type.UPDATE |
|
||||||
|
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
||||||
|
updatePullRequests(branchName)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call web hook
|
||||||
|
getWebHookURLs(owner, repository) match {
|
||||||
|
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||||
|
for(pusherAccount <- getAccountByUserName(pusher);
|
||||||
|
ownerAccount <- getAccountByUserName(owner);
|
||||||
|
repositoryInfo <- getRepository(owner, repository, baseUrl)){
|
||||||
|
callWebHook(owner, repository, webHookURLs,
|
||||||
|
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// update repository last modified time.
|
||||||
|
updateLastActivityDate(owner, repository)
|
||||||
|
} catch {
|
||||||
|
case ex: Exception => {
|
||||||
|
logger.error(ex.toString, ex)
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// update repository last modified time.
|
|
||||||
updateLastActivityDate(owner, repository)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def createIssueComment(commit: CommitInfo) = {
|
||||||
|
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
||||||
|
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
|
||||||
|
*/
|
||||||
|
private def updatePullRequests(branch: String) =
|
||||||
|
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
|
||||||
|
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
|
||||||
|
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
|
||||||
|
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
|
||||||
|
oldGit.fetch
|
||||||
|
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
|
||||||
|
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
|
||||||
|
.call
|
||||||
|
|
||||||
|
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
|
||||||
|
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||||
|
pullreq.userName, pullreq.repositoryName, pullreq.branch,
|
||||||
|
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
|
||||||
|
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user