mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-05 04:56:02 +01:00
Compare commits
1185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d3c7a0c61 | ||
|
|
7375ff9f97 | ||
|
|
5df6ec8985 | ||
|
|
9a8479ee58 | ||
|
|
73766f11eb | ||
|
|
a22878e2c5 | ||
|
|
1a2eb9d1e7 | ||
|
|
277ace3c8e | ||
|
|
40f376dbd9 | ||
|
|
444af0935e | ||
|
|
fb15fa0e43 | ||
|
|
bcd3e14870 | ||
|
|
c18702dcea | ||
|
|
1341ef9c52 | ||
|
|
f605a8d085 | ||
|
|
e0f7a7a3c6 | ||
|
|
1d72bed442 | ||
|
|
284b8e7c16 | ||
|
|
ff9fb24094 | ||
|
|
fde4448dd0 | ||
|
|
d16ce90a3d | ||
|
|
3ed5525956 | ||
|
|
855d1e12aa | ||
|
|
e03797a58f | ||
|
|
f0d38cf8ec | ||
|
|
2723580e17 | ||
|
|
1977aa481d | ||
|
|
4b36a8f831 | ||
|
|
96b56e38ba | ||
|
|
849d117ad3 | ||
|
|
8d57fca779 | ||
|
|
0dc867306b | ||
|
|
eefb4c01ec | ||
|
|
ccce499f7f | ||
|
|
9f11eaa4d3 | ||
|
|
7b85c0e55f | ||
|
|
7e92f1abd5 | ||
|
|
825f2518e9 | ||
|
|
def1e877db | ||
|
|
6acbd5b2cf | ||
|
|
73b7aef4a9 | ||
|
|
3d73e3922b | ||
|
|
224e44151f | ||
|
|
d9c1293985 | ||
|
|
849a40d4b5 | ||
|
|
177387e9b0 | ||
|
|
cacce54714 | ||
|
|
12082322ee | ||
|
|
7e0a5b7fec | ||
|
|
d2cf4afc81 | ||
|
|
cce62de075 | ||
|
|
d9450df7e9 | ||
|
|
41fc81fab6 | ||
|
|
aa35498bdd | ||
|
|
14becd0bd6 | ||
|
|
7390e21934 | ||
|
|
6b37967162 | ||
|
|
a03b9584ee | ||
|
|
34649dfeda | ||
|
|
bc84cfc2c8 | ||
|
|
bcba1f068b | ||
|
|
e6f30ef86b | ||
|
|
9e67999ef0 | ||
|
|
be75cef752 | ||
|
|
19ead71b48 | ||
|
|
7ebe1d6c62 | ||
|
|
2331b58b87 | ||
|
|
d495b04d85 | ||
|
|
751a8703ef | ||
|
|
1e6d26221d | ||
|
|
44a8e98c7b | ||
|
|
415519716e | ||
|
|
597f86dc7b | ||
|
|
579ed19949 | ||
|
|
9221bfa045 | ||
|
|
3057a31a6c | ||
|
|
d47ccf587c | ||
|
|
3e78d423ac | ||
|
|
0299cee5ec | ||
|
|
97ceffe689 | ||
|
|
9019d93449 | ||
|
|
32006e02c0 | ||
|
|
5ba0f6d51e | ||
|
|
c004d501f6 | ||
|
|
6067fa0fca | ||
|
|
e2c6658e59 | ||
|
|
0a6e50cbbe | ||
|
|
3732963d4b | ||
|
|
83bcbef6ce | ||
|
|
47cb4d1c49 | ||
|
|
32799cead7 | ||
|
|
dbedc2166b | ||
|
|
e86b404ca2 | ||
|
|
321a3a72f0 | ||
|
|
b512e7c390 | ||
|
|
ae7ead6272 | ||
|
|
db55719a6f | ||
|
|
4e1094e75b | ||
|
|
3fd97662f5 | ||
|
|
d6946b93c3 | ||
|
|
20217058fe | ||
|
|
e67365a19f | ||
|
|
c2f1817c6a | ||
|
|
4a948d9b01 | ||
|
|
377bc2703b | ||
|
|
196890b26f | ||
|
|
491fc2c164 | ||
|
|
1bed38f175 | ||
|
|
b124c31f65 | ||
|
|
8c588cbd66 | ||
|
|
334753b1ad | ||
|
|
f6e7401d1b | ||
|
|
ab564cc2d4 | ||
|
|
b10839a5c2 | ||
|
|
bb3323fb0e | ||
|
|
0b15ecbacd | ||
|
|
9f6afaed07 | ||
|
|
925420734e | ||
|
|
fb6bb12c52 | ||
|
|
22d12d0488 | ||
|
|
6888d959e1 | ||
|
|
65e66f52f6 | ||
|
|
225abfa126 | ||
|
|
876757f2d4 | ||
|
|
7e77398645 | ||
|
|
73c76a5a88 | ||
|
|
e5fca0d6cc | ||
|
|
eed7e5177f | ||
|
|
dd54ab31cb | ||
|
|
0085cb24ad | ||
|
|
6a758902ef | ||
|
|
0d81a9a9b6 | ||
|
|
6e4f6da633 | ||
|
|
15118ca5c1 | ||
|
|
8161560757 | ||
|
|
9ba564c864 | ||
|
|
06b5b92673 | ||
|
|
a417c373f1 | ||
|
|
5f5cc8d454 | ||
|
|
b9b6589bd7 | ||
|
|
b79f6a5fa0 | ||
|
|
0acbaeae86 | ||
|
|
bd046da3d0 | ||
|
|
a889ed7c46 | ||
|
|
e24684cb2b | ||
|
|
5f939c18b4 | ||
|
|
140f1eb31b | ||
|
|
d412dd5009 | ||
|
|
8643bfeb37 | ||
|
|
31b6adf0e5 | ||
|
|
f1ac2b3507 | ||
|
|
172af307a6 | ||
|
|
135e1ef73d | ||
|
|
da55bf6af3 | ||
|
|
883a9c8b17 | ||
|
|
7da89940e3 | ||
|
|
3233b0ae3c | ||
|
|
4c2ed09915 | ||
|
|
256b6c480f | ||
|
|
dc311837f9 | ||
|
|
92aec48c99 | ||
|
|
a6ada8c457 | ||
|
|
dcc601502e | ||
|
|
dd58d8c804 | ||
|
|
2ade54b7e3 | ||
|
|
136c5854f3 | ||
|
|
c597238d9c | ||
|
|
2552a58e08 | ||
|
|
74ad5872a3 | ||
|
|
485d502bd3 | ||
|
|
47bc8d030e | ||
|
|
48fe7133f7 | ||
|
|
5d962dc5e4 | ||
|
|
31e8e5a951 | ||
|
|
858373c628 | ||
|
|
7f142d2c0d | ||
|
|
08b86232a8 | ||
|
|
6bf4f42fdb | ||
|
|
f3c7de36d8 | ||
|
|
19f556de57 | ||
|
|
e4467df411 | ||
|
|
8d305a1fb1 | ||
|
|
b47153e645 | ||
|
|
c71766c84b | ||
|
|
23e4d679ae | ||
|
|
182acb2e02 | ||
|
|
b255b15006 | ||
|
|
b458f88161 | ||
|
|
398d8f2f1c | ||
|
|
85c1a56cbf | ||
|
|
da216c6960 | ||
|
|
bc91b153bf | ||
|
|
bc50b47d3a | ||
|
|
aed15a7f25 | ||
|
|
a1f09117b0 | ||
|
|
0a4a4a51ca | ||
|
|
f7fd53bf09 | ||
|
|
cbfb863a54 | ||
|
|
9d56d72611 | ||
|
|
527c91ff9d | ||
|
|
c58c2d6700 | ||
|
|
5518eca952 | ||
|
|
6e2b67ec0b | ||
|
|
837b1e44a7 | ||
|
|
e04c230c6e | ||
|
|
a01b5a4a59 | ||
|
|
427b6ce846 | ||
|
|
b7b5af2b72 | ||
|
|
39fec57f72 | ||
|
|
238dedb6df | ||
|
|
af091117b7 | ||
|
|
ddea4e12f0 | ||
|
|
9767903252 | ||
|
|
bc75f9f8a2 | ||
|
|
63627fc1d0 | ||
|
|
c23985c1a7 | ||
|
|
af58e99dcf | ||
|
|
676670e9e3 | ||
|
|
823c52e941 | ||
|
|
7f42007648 | ||
|
|
7214ef21d2 | ||
|
|
18a4492975 | ||
|
|
99f73b1016 | ||
|
|
0c1ce6a088 | ||
|
|
ae6291ab83 | ||
|
|
617fcf7c99 | ||
|
|
9df4a74837 | ||
|
|
966d4251be | ||
|
|
84b2e9cdcd | ||
|
|
e29d63c91a | ||
|
|
805d2b8e79 | ||
|
|
9983fd1292 | ||
|
|
1de202e927 | ||
|
|
4eb9f4a485 | ||
|
|
a8801e4e41 | ||
|
|
ee1c84dbf2 | ||
|
|
e40e1fa6cd | ||
|
|
055f648ea2 | ||
|
|
37a399c3a2 | ||
|
|
bc0b11b60a | ||
|
|
65a1ca7146 | ||
|
|
2293030d4e | ||
|
|
2848f07b83 | ||
|
|
55224ddcd8 | ||
|
|
054ae75b6b | ||
|
|
c83fab611e | ||
|
|
29baf1223c | ||
|
|
2a60f607ff | ||
|
|
78f4d26aa0 | ||
|
|
f59e86f5ca | ||
|
|
1c2af36c92 | ||
|
|
badbe73f4e | ||
|
|
a9d58698cd | ||
|
|
bb3f086aa6 | ||
|
|
2db674bb03 | ||
|
|
4bc4a16a80 | ||
|
|
d88a105628 | ||
|
|
15d0c5b506 | ||
|
|
dbde79d2f2 | ||
|
|
e6e3786b47 | ||
|
|
4c1b8004fc | ||
|
|
ff4052f097 | ||
|
|
13c206d068 | ||
|
|
5b875d7c73 | ||
|
|
e33dd9008b | ||
|
|
8764910553 | ||
|
|
4c89c40944 | ||
|
|
0f0986afcf | ||
|
|
5d5f1f8bdd | ||
|
|
03e386b3ce | ||
|
|
435eac7ae6 | ||
|
|
bd5df3977d | ||
|
|
ba218053f9 | ||
|
|
1fe448a83b | ||
|
|
26a45d0117 | ||
|
|
320585a530 | ||
|
|
ca0f888a99 | ||
|
|
3b08dc2e41 | ||
|
|
cc128a49c1 | ||
|
|
e0148695f2 | ||
|
|
afe0b1dd71 | ||
|
|
353852d6da | ||
|
|
28585d1a3d | ||
|
|
9d69a48c65 | ||
|
|
2f95c76634 | ||
|
|
eac9f0e6ff | ||
|
|
043fc21e05 | ||
|
|
5854a75615 | ||
|
|
7b02946496 | ||
|
|
70f0ffd4f4 | ||
|
|
91b82c2652 | ||
|
|
b1017140aa | ||
|
|
fc806b8813 | ||
|
|
836913482b | ||
|
|
b3df3f44c6 | ||
|
|
4ffbf89e74 | ||
|
|
9851c7d93d | ||
|
|
a10188260c | ||
|
|
2201f2b202 | ||
|
|
c92e71bb7a | ||
|
|
d271fac350 | ||
|
|
ce4522fc30 | ||
|
|
a178c48de6 | ||
|
|
9d1323a044 | ||
|
|
43babfed94 | ||
|
|
6fa7ea30fb | ||
|
|
d78315695b | ||
|
|
16021865cb | ||
|
|
b516be242d | ||
|
|
0124f7cc3c | ||
|
|
f3eec35287 | ||
|
|
fb396a33b0 | ||
|
|
3370499421 | ||
|
|
d847e27cf9 | ||
|
|
9684b158ce | ||
|
|
8456808a8e | ||
|
|
9747899a19 | ||
|
|
099304605e | ||
|
|
30994d0465 | ||
|
|
71fdbe7b71 | ||
|
|
86432c5ffe | ||
|
|
4dfa1fb0f8 | ||
|
|
db59a7652f | ||
|
|
417470a81c | ||
|
|
cc639da17e | ||
|
|
f619f4a9bc | ||
|
|
5dffc2a64e | ||
|
|
bb63a8d14c | ||
|
|
c1263cc16d | ||
|
|
49f2e7d70f | ||
|
|
f93b535f70 | ||
|
|
e16d3c823b | ||
|
|
7a6fdbcf50 | ||
|
|
46041a3762 | ||
|
|
20b0553f7f | ||
|
|
5870cacf44 | ||
|
|
cb512cd98d | ||
|
|
90487eb7b7 | ||
|
|
706fa77de3 | ||
|
|
26b14ded58 | ||
|
|
3b1367dd8e | ||
|
|
e1f310317d | ||
|
|
937814ec5d | ||
|
|
b55fc649a6 | ||
|
|
f4e4506517 | ||
|
|
287a0b6669 | ||
|
|
5bddd352af | ||
|
|
9c6ea8fb9d | ||
|
|
32e8bf46a7 | ||
|
|
d61fe1bf84 | ||
|
|
47dbea947d | ||
|
|
97c6b0495e | ||
|
|
a602ece8e9 | ||
|
|
cf6dca84d8 | ||
|
|
79432ff8ad | ||
|
|
b8613431de | ||
|
|
698eafa562 | ||
|
|
d33886db89 | ||
|
|
cde09d3a59 | ||
|
|
5674f0e980 | ||
|
|
b9ade60eb2 | ||
|
|
96303723fa | ||
|
|
0f5dbc5788 | ||
|
|
8df0c3a439 | ||
|
|
ca6a86816a | ||
|
|
3ea939798f | ||
|
|
d947410e3c | ||
|
|
db59bc08ac | ||
|
|
95a8649f79 | ||
|
|
ffd10122ed | ||
|
|
c4c39f36e9 | ||
|
|
96900c3cbf | ||
|
|
69fa370d12 | ||
|
|
7496437d11 | ||
|
|
33b7d09af7 | ||
|
|
53d0974760 | ||
|
|
a87399f223 | ||
|
|
975dfb17e1 | ||
|
|
8b8bd0289b | ||
|
|
3bb69c623b | ||
|
|
dd427bdbef | ||
|
|
b40657a14a | ||
|
|
21ca5b2eec | ||
|
|
b78d584d8a | ||
|
|
e6b666a66a | ||
|
|
bab93ea4f5 | ||
|
|
7fe98253ae | ||
|
|
13385cbced | ||
|
|
3f20cec7b2 | ||
|
|
a0e4b020ca | ||
|
|
ea5d898b27 | ||
|
|
4e652b5ccd | ||
|
|
dd809896c8 | ||
|
|
93536d3365 | ||
|
|
098b18fe6d | ||
|
|
66efdac757 | ||
|
|
45545d3815 | ||
|
|
b65d41731b | ||
|
|
be19e97518 | ||
|
|
2ebf2b99bd | ||
|
|
be79ac2eb2 | ||
|
|
05afec3236 | ||
|
|
57879eb72e | ||
|
|
2bc915f51b | ||
|
|
1ca55805b5 | ||
|
|
93cc1be166 | ||
|
|
f88ce3f671 | ||
|
|
20aabfc273 | ||
|
|
601f8c4249 | ||
|
|
d0ccfc52b8 | ||
|
|
c22aef8ee2 | ||
|
|
3807e61a48 | ||
|
|
55722f87af | ||
|
|
212f3725ed | ||
|
|
193a312b22 | ||
|
|
6a2d2ebfd1 | ||
|
|
82beed1f44 | ||
|
|
0ede7e9921 | ||
|
|
6d200aa340 | ||
|
|
a0fbb90048 | ||
|
|
08e29e7077 | ||
|
|
d2317d0a97 | ||
|
|
972628eb65 | ||
|
|
51a56356cb | ||
|
|
3bef71f5f2 | ||
|
|
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 | ||
|
|
6175eb7c08 | ||
|
|
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 | ||
|
|
ebb9d9329a | ||
|
|
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 | ||
|
|
843722f82e | ||
|
|
ce79eaada8 | ||
|
|
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 | ||
|
|
38df990033 | ||
|
|
940e2f4759 | ||
|
|
6fe65c76b1 | ||
|
|
c0713eaeda | ||
|
|
000afa1ed6 | ||
|
|
828688ddd0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ project/plugins/project/
|
|||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
.cache
|
.cache
|
||||||
|
.settings
|
||||||
|
|
||||||
# IntelliJ specific
|
# IntelliJ specific
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
language: scala
|
||||||
|
scala:
|
||||||
|
- 2.11.6
|
||||||
268
README.md
268
README.md
@@ -1,12 +1,15 @@
|
|||||||
GitBucket
|
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://travis-ci.org/takezoe/gitbucket)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
GitBucket is the easily installable Github clone written with Scala.
|
GitBucket is the easily installable GitHub clone powered by 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)
|
- Repository search (Code and Issues)
|
||||||
- Wiki
|
- Wiki
|
||||||
- Issues
|
- Issues
|
||||||
@@ -15,11 +18,13 @@ The current version of GitBucket provides a basic features below:
|
|||||||
- Activity timeline
|
- Activity timeline
|
||||||
- User management (for Administrators)
|
- User management (for Administrators)
|
||||||
- Group (like Organization in Github)
|
- 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!
|
||||||
|
|
||||||
- Network graph
|
- Network graph
|
||||||
- Statics
|
- Statistics
|
||||||
- Watch / Star
|
- Watch / Star
|
||||||
|
|
||||||
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).
|
||||||
@@ -28,55 +33,240 @@ 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**.
|
||||||
|
|
||||||
To upgrade GitBucket, only replace gitbucket.war.
|
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
|
||||||
--------
|
--------
|
||||||
|
### 3.1 - 28 Mar 2015
|
||||||
|
- Web APIs for Jenkins github pull-request builder
|
||||||
|
- Improved diff view
|
||||||
|
- Bump Scalatra to 2.3.1, sbt to 0.13.8
|
||||||
|
|
||||||
|
### 3.0 - 3 Mar 2015
|
||||||
|
- New plug-in system is available
|
||||||
|
- Connection pooling by c3p0
|
||||||
|
- New branch UI
|
||||||
|
- Compare between specified commit ids
|
||||||
|
|
||||||
|
### 2.8 - 1 Feb 2015
|
||||||
|
- New logo and icons
|
||||||
|
- New system setting options to control visibility
|
||||||
|
- Comment on side-by-side diff
|
||||||
|
- Information message on sign-in page
|
||||||
|
- Fork repository by group account
|
||||||
|
|
||||||
|
### 2.7 - 29 Dec 2014
|
||||||
|
- Comment for commit and diff
|
||||||
|
- Fix security issue in markdown rendering
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.6 - 24 Nov 2014
|
||||||
|
- Search box at issues and pull requests
|
||||||
|
- Information from administrator
|
||||||
|
- Pull request UI has been updated
|
||||||
|
- Move to TravisCI from Buildhive
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.5 - 4 Nov 2014
|
||||||
|
- New Dashboard
|
||||||
|
- Change datetime format
|
||||||
|
- Create branch from Web UI
|
||||||
|
- Task list in Markdown
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.4.1 - 6 Oct 2014
|
||||||
|
- Bug fix
|
||||||
|
|
||||||
|
### 2.4 - 6 Oct 2014
|
||||||
|
- New UI is applied to Issues and Pull requests
|
||||||
|
- Side-by-side diff is available
|
||||||
|
- Fix relative path problem in Markdown links and images
|
||||||
|
- Plugin System is disabled in default
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.3 - 1 Sep 2014
|
||||||
|
- Scala based plugin system
|
||||||
|
- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.2.1 - 5 Aug 2014
|
||||||
|
- Bug fix
|
||||||
|
|
||||||
|
### 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
|
### 1.5 - 4 Sep 2013
|
||||||
- Fork and pull request.
|
- Fork and pull request
|
||||||
- LDAP authentication.
|
- LDAP authentication
|
||||||
- Mail notification.
|
- Mail notification
|
||||||
- Add an option to turn off the gravatar support.
|
- Add an option to turn off the gravatar support
|
||||||
- Add the branch tab in the repository viewer.
|
- Add the branch tab in the repository viewer
|
||||||
- Encoding auto detection for the file content in the repository viewer.
|
- Encoding auto detection for the file content in the repository viewer
|
||||||
- Add favicon, header logo and icons for the timeline.
|
- Add favicon, header logo and icons for the timeline
|
||||||
- Specify data directory via environment variable GITBUCKET_HOME.
|
- Specify data directory via environment variable GITBUCKET_HOME
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.4 - 31 Jul 2013
|
### 1.4 - 31 Jul 2013
|
||||||
- Group management.
|
- Group management
|
||||||
- Repository search for code and issues.
|
- Repository search for code and issues
|
||||||
- Display user related issues on the dashboard.
|
- Display user related issues on the dashboard
|
||||||
- Display participants avatar of issues on the issue page.
|
- Display participants avatar of issues on the issue page
|
||||||
- Performance improvement for repository viewer.
|
- Performance improvement for repository viewer
|
||||||
- Alert by milestone due date.
|
- Alert by milestone due date
|
||||||
- H2 database administration console.
|
- H2 database administration console
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.3 - 18 Jul 2013
|
### 1.3 - 18 Jul 2013
|
||||||
- Batch updating for issues.
|
- Batch updating for issues
|
||||||
- Display assigned user on issue list.
|
- Display assigned user on issue list
|
||||||
- User icon and Gravatar support.
|
- User icon and Gravatar support
|
||||||
- Convert @xxxx to link to the account page.
|
- Convert @xxxx to link to the account page
|
||||||
- Add copy to clipboard button for git clone URL.
|
- Add copy to clipboard button for git clone URL
|
||||||
- Allows multi-byte characters as wiki page name.
|
- Allow multi-byte characters as wiki page name
|
||||||
- Allows to create the empty repository.
|
- Allow to create the empty repository
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.2 - 09 Jul 2013
|
### 1.2 - 09 Jul 2013
|
||||||
- Added activity timeline.
|
- Add activity timeline
|
||||||
- Bugfix for Git 1.8.1.5 or later.
|
- Bugfix for Git 1.8.1.5 or later
|
||||||
- Allows multi-byte characters as label.
|
- Allow multi-byte characters as label
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
|
|
||||||
### 1.1 - 05 Jul 2013
|
### 1.1 - 05 Jul 2013
|
||||||
- Fixed some bugs.
|
- Fix some bugs
|
||||||
- Upgrade to JGit 3.0.
|
- 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="3.0.0"/>
|
||||||
|
<property name="jetty.version" value="8.1.16.v20140903"/>
|
||||||
|
<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>
|
||||||
13
contrib/README.md
Normal file
13
contrib/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Contrib Notes #
|
||||||
|
|
||||||
|
The configuration script adapts according to the OS.
|
||||||
|
The `linux` directory contains scripts for Ubuntu and RedHat.
|
||||||
|
The Mac scripts have been folded in as well.
|
||||||
|
Common scripts are in this directory.
|
||||||
|
|
||||||
|
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
1. Edit `gitbucket.conf` to suit.
|
||||||
|
2. Type: `install`
|
||||||
62
contrib/gitbucket.conf
Normal file
62
contrib/gitbucket.conf
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Configuration section is below. Ignore this part
|
||||||
|
|
||||||
|
function isUbuntu {
|
||||||
|
if [ -f /etc/lsb-release ]; then
|
||||||
|
grep -i ubuntu /etc/lsb-release | head -n 1 | cut -d \ -f 1 | cut -d = -f 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRedHat {
|
||||||
|
if [ -d "/etc/rc.d/init.d" ]; then echo yes; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMac {
|
||||||
|
if [[ "$(uname -a | cut -d \ -f 1 )" == "Darwin" ]]; then echo yes; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Configuration section start
|
||||||
|
#
|
||||||
|
|
||||||
|
# Bind host
|
||||||
|
GITBUCKET_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Other Java option
|
||||||
|
GITBUCKET_JVM_OPTS=-Dmail.smtp.starttls.enable=true
|
||||||
|
|
||||||
|
# Data directory, holds repositories
|
||||||
|
GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
|
||||||
|
GITBUCKET_LOG_DIR=/var/log/gitbucket
|
||||||
|
|
||||||
|
# Server port
|
||||||
|
GITBUCKET_PORT=8080
|
||||||
|
|
||||||
|
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
|
||||||
|
GITBUCKET_PREFIX=
|
||||||
|
|
||||||
|
# Directory where GitBucket is installed
|
||||||
|
# Configuration is stored here:
|
||||||
|
GITBUCKET_DIR=/usr/share/gitbucket
|
||||||
|
GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
|
||||||
|
|
||||||
|
# Path to the WAR file
|
||||||
|
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
|
||||||
|
|
||||||
|
# GitBucket version to fetch when installing
|
||||||
|
GITBUCKET_VERSION=2.1
|
||||||
|
|
||||||
|
#
|
||||||
|
# End of configuration section. Ignore this part
|
||||||
|
#
|
||||||
|
if [ `isUbuntu` ]; then
|
||||||
|
GITBUCKET_SERVICE=/etc/init.d/gitbucket
|
||||||
|
elif [ `isRedHat` ]; then
|
||||||
|
GITBUCKET_SERVICE=/etc/rc.d/init.d
|
||||||
|
elif [ `isMac` ]; then
|
||||||
|
GITBUCKET_SERVICE=/Library/StartupItems/GitBucket/GitBucket
|
||||||
|
else
|
||||||
|
echo "Don't know how to install onto this OS"
|
||||||
|
exit -2
|
||||||
|
fi
|
||||||
|
|
||||||
138
contrib/gitbucket.init
Normal file
138
contrib/gitbucket.init
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# RedHat: /etc/rc.d/init.d/gitbucket
|
||||||
|
# Ubuntu: /etc/init.d/gitbucket
|
||||||
|
# Mac OS/X: /Library/StartupItems/GitBucket
|
||||||
|
#
|
||||||
|
# Starts the GitBucket server
|
||||||
|
#
|
||||||
|
# chkconfig: 345 60 40
|
||||||
|
# description: Run GitBucket server
|
||||||
|
# processname: java
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
|
||||||
|
[ -f /etc/rc.common ] && source /etc/rc.common # Mac OS/X
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# Pull in cq settings
|
||||||
|
[ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
|
||||||
|
[ -f gitbucket.conf ] && source gitbucket.conf # For all systems
|
||||||
|
|
||||||
|
# Location of the log and PID file
|
||||||
|
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
|
||||||
|
PID_FILE=/var/run/gitbucket.pid
|
||||||
|
|
||||||
|
RED='\033[1m\E[37;41m'
|
||||||
|
GREEN='\033[1m\E[37;42m'
|
||||||
|
OFF='\E[0m'
|
||||||
|
|
||||||
|
if [ -z "$(which success)" ]; then
|
||||||
|
function success {
|
||||||
|
printf "%b\n" "$GREEN $* $OFF"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
if [ -z "$(which failure)" ]; then
|
||||||
|
function failure {
|
||||||
|
printf "%b\n" "$RED $* $OFF"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
RETVAL=0
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo -n $"Starting GitBucket server: "
|
||||||
|
|
||||||
|
START_OPTS=
|
||||||
|
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
|
||||||
|
|
||||||
|
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
echo $! > $PID_FILE
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "Success"
|
||||||
|
else
|
||||||
|
failure "Exit code $RETVAL"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
## MacOS proxies for System V service hooks:
|
||||||
|
StartService() {
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
StopService() {
|
||||||
|
stop
|
||||||
|
}
|
||||||
|
|
||||||
|
RestartService() {
|
||||||
|
restart
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if [ `isMac` ]; then
|
||||||
|
RunService "$1"
|
||||||
|
else
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
69
contrib/install
Executable file
69
contrib/install
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Only tested on Ubuntu 14.04
|
||||||
|
|
||||||
|
# Uses information stored in GitBucket git repo on GitHub as defaults.
|
||||||
|
# Edit gitbucket.conf before running this
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GITBUCKET_VERSION=2.1
|
||||||
|
|
||||||
|
if [ ! -f gitbucket.conf ]; then
|
||||||
|
echo "gitbucket.conf not found, aborting"
|
||||||
|
exit -3
|
||||||
|
fi
|
||||||
|
source gitbucket.conf
|
||||||
|
|
||||||
|
function createDir {
|
||||||
|
if [ ! -d "$1" ]; then
|
||||||
|
echo "Making $1 directory."
|
||||||
|
sudo mkdir -p "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$(which iptables)" ]; then
|
||||||
|
echo "Opening port $GITBUCKET_PORT in firewall."
|
||||||
|
sudo iptables -A INPUT -p tcp --dport $GITBUCKET_PORT -j ACCEPT
|
||||||
|
echo "Please use iptables-persistent:"
|
||||||
|
echo " sudo apt-get install iptables-persistent"
|
||||||
|
echo "After installed, you can save/reload iptables rules anytime:"
|
||||||
|
echo " sudo /etc/init.d/iptables-persistent save"
|
||||||
|
echo " sudo /etc/init.d/iptables-persistent reload"
|
||||||
|
fi
|
||||||
|
|
||||||
|
createDir "$GITBUCKET_HOME"
|
||||||
|
createDir "$GITBUCKET_WAR_DIR"
|
||||||
|
createDir "$GITBUCKET_DIR"
|
||||||
|
createDir "$GITBUCKET_LOG_DIR"
|
||||||
|
|
||||||
|
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
|
||||||
|
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
|
||||||
|
|
||||||
|
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
|
||||||
|
|
||||||
|
echo "Copying gitbucket.conf to $GITBUCKET_DIR"
|
||||||
|
sudo cp gitbucket.conf $GITBUCKET_DIR
|
||||||
|
if [ `isUbuntu` ] || [ `isRedHat` ]; then
|
||||||
|
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
|
||||||
|
# Install gitbucket as a service that starts when system boots
|
||||||
|
sudo chown root:root $GITBUCKET_SERVICE
|
||||||
|
sudo chmod 755 $GITBUCKET_SERVICE
|
||||||
|
sudo update-rc.d "$(basename $GITBUCKET_SERVICE)" defaults 98 02
|
||||||
|
echo "Starting GitBucket service"
|
||||||
|
sudo $GITBUCKET_SERVICE start
|
||||||
|
elif [ `isMac` ]; then
|
||||||
|
sudo macosx/makePlist
|
||||||
|
echo "Starting GitBucket service"
|
||||||
|
sudo cp gitbucket.conf "$GITBUCKET_SERVICE"
|
||||||
|
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
|
||||||
|
sudo chmod a+x "$GITBUCKET_SERVICE"
|
||||||
|
sudo "$GITBUCKET_SERVICE" start
|
||||||
|
else
|
||||||
|
echo "Don't know how to install this OS"
|
||||||
|
exit -2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
less "$GITBUCKET_LOG_DIR/run.log"
|
||||||
|
fi
|
||||||
15
contrib/linux/redhat/README.md
Normal file
15
contrib/linux/redhat/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Contrib Notes #
|
||||||
|
|
||||||
|
RPM spec file and init script for Red Hat Enterprise Linux 6.x.
|
||||||
|
|
||||||
|
To create RPM:
|
||||||
|
1. Edit `../../gitbucket.conf` to suit.
|
||||||
|
2. Edit `gitbucket.init` to suit.
|
||||||
|
3. Edit `gitbucket.spec` to suit.
|
||||||
|
4. Place `gitbucket.spec` to rpm/SPECS/.
|
||||||
|
5. Place `gitbucket.init` and `gitbucket.war` to rpm/SOURCES/.
|
||||||
|
6. Execute `rpmbuild -ba rpm/SPECS/gitbucket.spec`
|
||||||
|
|
||||||
|
This rpm runs gitbucket not as root user but as gitbucket user.
|
||||||
|
This rpm creates user and group named `gitbucket` at installation.
|
||||||
|
This rpm make chkconfig of gitbucket to be on.
|
||||||
108
contrib/linux/redhat/gitbucket.init
Normal file
108
contrib/linux/redhat/gitbucket.init
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# RedHat: /etc/rc.d/init.d/gitbucket
|
||||||
|
#
|
||||||
|
# Starts the GitBucket server
|
||||||
|
#
|
||||||
|
# chkconfig: 345 60 40
|
||||||
|
# description: Run GitBucket server
|
||||||
|
# processname: java
|
||||||
|
|
||||||
|
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# Pull in cq settings
|
||||||
|
[ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
|
||||||
|
[ -f gitbucket.conf ] && source gitbucket.conf # For all systems
|
||||||
|
|
||||||
|
# Location of the log and PID file
|
||||||
|
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
|
||||||
|
|
||||||
|
RED='\033[1m\E[37;41m'
|
||||||
|
GREEN='\033[1m\E[37;42m'
|
||||||
|
OFF='\E[0m'
|
||||||
|
|
||||||
|
RETVAL=0
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo -n $"Starting GitBucket server: "
|
||||||
|
|
||||||
|
START_OPTS=
|
||||||
|
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
|
||||||
|
|
||||||
|
GITBUCKET_HOME="${GITBUCKET_HOME}" daemon --user=gitbucket java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
|
||||||
|
sleep 3
|
||||||
|
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "Success"
|
||||||
|
else
|
||||||
|
failure "Exit code $RETVAL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -n $"Stopping GitBucket server: "
|
||||||
|
|
||||||
|
# Run the Java process
|
||||||
|
pkill -f $GITBUCKET_WAR_FILE >>$LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "GitBucket stopping"
|
||||||
|
else
|
||||||
|
failure "GitBucket stopping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
if [ $RETVAL -eq 0 ]; then
|
||||||
|
echo $"GitBucket is running...."
|
||||||
|
else
|
||||||
|
echo $"GitBucket is stopped"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo $"Usage: $0 [start|stop|restart|status]"
|
||||||
|
RETVAL=2
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit $RETVAL
|
||||||
|
|
||||||
82
contrib/linux/redhat/gitbucket.spec
Normal file
82
contrib/linux/redhat/gitbucket.spec
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
Name: gitbucket
|
||||||
|
Summary: GitHub clone written with Scala.
|
||||||
|
Version: 2.6
|
||||||
|
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
|
||||||
|
|
||||||
|
%pre
|
||||||
|
/usr/sbin/groupadd -r gitbucket &> /dev/null || :
|
||||||
|
/usr/sbin/useradd -g gitbucket -s /bin/false -r -c "GitBucket GitHub clone" -d %{_sharedstatedir}/%{name} gitbucket &> /dev/null || :
|
||||||
|
|
||||||
|
%post
|
||||||
|
/sbin/chkconfig --add gitbucket
|
||||||
|
|
||||||
|
%preun
|
||||||
|
if [ "$1" = 0 ]; then
|
||||||
|
/sbin/service gitbucket stop > /dev/null 2>&1
|
||||||
|
/sbin/chkconfig --del gitbucket
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
%postun
|
||||||
|
if [ "$1" -ge 1 ]; then
|
||||||
|
/sbin/service gitbucket restart > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
%clean
|
||||||
|
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
||||||
|
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(-,root,root,-)
|
||||||
|
%{_datarootdir}/%{name}/lib/%{name}.war
|
||||||
|
%config %{_sysconfdir}/init.d/%{name}
|
||||||
|
%config(noreplace) %{_sysconfdir}/sysconfig/%{name}
|
||||||
|
%attr(0755,gitbucket,gitbucket) %{_sharedstatedir}/%{name}
|
||||||
|
%attr(0750,gitbucket,gitbucket) %{_localstatedir}/log/%{name}
|
||||||
|
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Mon Nov 24 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.6
|
||||||
|
|
||||||
|
* Sun Nov 09 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.5
|
||||||
|
|
||||||
|
* Sun Oct 26 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.4.1
|
||||||
|
|
||||||
|
* Mon Jul 21 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- execute as gitbucket user
|
||||||
|
|
||||||
|
* Sun Jul 20 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.1.
|
||||||
|
|
||||||
|
* 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.
|
||||||
28
contrib/macosx/makePlist
Executable file
28
contrib/macosx/makePlist
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# From http://docstore.mik.ua/orelly/unix3/mac/ch02_02.htm
|
||||||
|
source gitbucket.conf
|
||||||
|
GITBUCKET_SERVICE_DIR=`dirname "$GITBUCKET_SERVICE"`
|
||||||
|
mkdir -p "$GITBUCKET_SERVICE_DIR"
|
||||||
|
cat << EOF > "$GITBUCKET_SERVICE_DIR/gitbucket.plist"
|
||||||
|
<?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>$GITBUCKET_JVM_OPTS</string>
|
||||||
|
<string>-jar</string>
|
||||||
|
<string>gitbucket.war</string>
|
||||||
|
<string>--host=$GITBUCKET_HOST</string>
|
||||||
|
<string>--port=$GITBUCKET_PORT</string>
|
||||||
|
<string>--https=true</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
22
doc/activity.md
Normal file
22
doc/activity.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Activity Timeline
|
||||||
|
========
|
||||||
|
GitBucket records several types of user activity to ```ACTIVITY``` table. Activity types are shown below:
|
||||||
|
|
||||||
|
type | message | additional information
|
||||||
|
------------------|------------------------------------------------------|------------------------
|
||||||
|
create_repository |$user created $owner/$repo |-
|
||||||
|
open_issue |$user opened issue $owner/$repo#$issueId |-
|
||||||
|
close_issue |$user closed issue $owner/$repo#$issueId |-
|
||||||
|
close_issue |$user closed pull request $owner/$repo#$issueId |-
|
||||||
|
reopen_issue |$user reopened issue $owner/$repo#$issueId |-
|
||||||
|
comment_issue |$user commented on issue $owner/$repo#$issueId |-
|
||||||
|
comment_issue |$user commented on pull request $owner/$repo#$issueId |-
|
||||||
|
create_wiki |$user created the $owner/$repo wiki |$page
|
||||||
|
edit_wiki |$user edited the $owner/$repo wiki |$page<br>$page:$commitId(since 1.5)
|
||||||
|
push |$user pushed to $owner/$repo#$branch to $owner/$repo |$commitId:$shortMessage\n*
|
||||||
|
create_tag |$user created tag $tag at $owner/$repo |-
|
||||||
|
create_branch |$user created branch $branch at $owner/$repo |-
|
||||||
|
delete_branch |$user deleted branch $branch at $owner/$repo |-
|
||||||
|
fork |$user forked $owner/$repo to $owner/$repo |-
|
||||||
|
open_pullreq |$user opened pull request $owner/$repo#issueId |-
|
||||||
|
merge_pullreq |$user merge pull request $owner/$repo#issueId |-
|
||||||
37
doc/auto_update.md
Normal file
37
doc/auto_update.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
Automatic Schema Updating
|
||||||
|
========
|
||||||
|
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading.
|
||||||
|
|
||||||
|
To release a new version of GitBucket, add the version definition to the [servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/servlet/AutoUpdateListener.scala) at first.
|
||||||
|
|
||||||
|
```scala
|
||||||
|
object AutoUpdate {
|
||||||
|
...
|
||||||
|
/**
|
||||||
|
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||||
|
*/
|
||||||
|
val versions = Seq(
|
||||||
|
Version(1, 0)
|
||||||
|
)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/takezoe/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
|
||||||
|
|
||||||
|
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
|
||||||
|
|
||||||
|
We can also add any Scala code for upgrade GitBucket which modifies resources other than database. Override ```Version.update``` like below:
|
||||||
|
|
||||||
|
```scala
|
||||||
|
val versions = Seq(
|
||||||
|
new Version(1, 3){
|
||||||
|
override def update(conn: Connection): Unit = {
|
||||||
|
super.update(conn)
|
||||||
|
// Add any code here!
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Version(1, 2),
|
||||||
|
Version(1, 1),
|
||||||
|
Version(1, 0)
|
||||||
|
)
|
||||||
|
```
|
||||||
48
doc/comment_action.md
Normal file
48
doc/comment_action.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
About Action in Issue Comment
|
||||||
|
========
|
||||||
|
After the issue creation at GitBucket, users can add comments or close it.
|
||||||
|
The details are saved at ```ISSUE_COMMENT``` table.
|
||||||
|
|
||||||
|
To determine if it was any operation, you see the ```ACTION``` column.
|
||||||
|
|
||||||
|
|ACTION|
|
||||||
|
|--------|
|
||||||
|
|comment|
|
||||||
|
|close_comment|
|
||||||
|
|reopen_comment|
|
||||||
|
|close|
|
||||||
|
|reopen|
|
||||||
|
|commit|
|
||||||
|
|merge|
|
||||||
|
|delete_branch|
|
||||||
|
|refer|
|
||||||
|
|
||||||
|
#####comment
|
||||||
|
This value is saved when users have made a normal comment.
|
||||||
|
|
||||||
|
#####close_comment, reopen_comment
|
||||||
|
These values are saved when users have reopened or closed the issue with comments.
|
||||||
|
|
||||||
|
#####close, reopen
|
||||||
|
These values are saved when users have reopened or closed the issue.
|
||||||
|
At the same time, store the fixed value(i.e. "Close" or "Reopen") to the ```CONTENT``` column.
|
||||||
|
Therefore, this comment is not displayed, and not counted as a comment.
|
||||||
|
|
||||||
|
#####commit
|
||||||
|
This value is saved when users have pushed including the ```#issueId``` to the commit message.
|
||||||
|
At the same time, store it to the ```CONTENT``` column with its commit id.
|
||||||
|
This comment is displayed. But it can not be edited by all users, and also not counted as a comment.
|
||||||
|
|
||||||
|
#####merge
|
||||||
|
This value is saved when users have merged the pull request.
|
||||||
|
At the same time, store the message to the ```CONTENT``` column.
|
||||||
|
This comment is displayed. But it can not be edited by all users, and also not counted as a comment.
|
||||||
|
|
||||||
|
#####delete_branch
|
||||||
|
This value is saved when users have deleted the branch. Users can delete branch after merging pull request which is requested from the same repository.
|
||||||
|
At the same time, store it to the ```CONTENT``` column with the deleted branch name.
|
||||||
|
Therefore, this comment is not displayed, and not counted as a comment.
|
||||||
|
|
||||||
|
#####refer
|
||||||
|
This value is saved when other issue or issue comment contains reference to the issue like ```#issueId```.
|
||||||
|
At the same time, store id and title of the referrer issue as ```id:title```.
|
||||||
44
doc/directory.md
Normal file
44
doc/directory.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
Directory Structure
|
||||||
|
========
|
||||||
|
GitBucket persists all data into __HOME/.gitbucket__ in default (In 1.9 or before, HOME/gitbucket is default).
|
||||||
|
|
||||||
|
This directory has following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
* /HOME/gitbucket
|
||||||
|
* /repositoties
|
||||||
|
* /USER_NAME
|
||||||
|
* / REPO_NAME.git (substance of repository. GitServlet sees this directory)
|
||||||
|
* / REPO_NAME
|
||||||
|
* /issues (files which are attached to issue)
|
||||||
|
* / REPO_NAME.wiki.git (wiki repository)
|
||||||
|
* /data
|
||||||
|
* /USER_NAME
|
||||||
|
* /files
|
||||||
|
* avatar.xxx (image file of user avatar)
|
||||||
|
* /plugins
|
||||||
|
* /PLUGIN_NAME
|
||||||
|
* plugin.js
|
||||||
|
* /tmp
|
||||||
|
* /_upload
|
||||||
|
* /SESSION_ID (removed at session timeout)
|
||||||
|
* current time millis + random 10 alphanumeric chars (temporary file for file uploading)
|
||||||
|
* /USER_NAME
|
||||||
|
* /init-REPO_NAME (used in repository creation and removed after it) ... unused since 1.8
|
||||||
|
* /REPO_NAME.wiki (working directory for wiki repository) ... unused since 1.8
|
||||||
|
* /REPO_NAME
|
||||||
|
* /download (temporary directories are created under this directory)
|
||||||
|
```
|
||||||
|
|
||||||
|
There are some ways to specify the data directory instead of the default location.
|
||||||
|
|
||||||
|
1. Environment variable __GITBUCKET_HOME__
|
||||||
|
2. System property __gitbucket.home__ (e.g. ```-Dgitbucket.home=PATH_TO_DATADIR```)
|
||||||
|
3. Command line option for embedded Jetty (e.g. ```java -jar gitbucket.war --data=PATH_TO_DATADIR```)
|
||||||
|
4. Context parameter __gitbucket.home__ in web.xml like below:
|
||||||
|
```xml
|
||||||
|
<context-param>
|
||||||
|
<param-name>gitbucket.home</param-name>
|
||||||
|
<param-value>PATH_TO_DATADIR</param-value>
|
||||||
|
</context-param>
|
||||||
|
```
|
||||||
38
doc/how_to_run.md
Normal file
38
doc/how_to_run.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
How to run from the source tree
|
||||||
|
========
|
||||||
|
|
||||||
|
for Testers
|
||||||
|
--------
|
||||||
|
|
||||||
|
If you want to test GitBucket, input following command at the root directory of the source tree.
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\gitbucket> sbt ~container:start
|
||||||
|
```
|
||||||
|
|
||||||
|
Then access to `http://localhost:8080/` by your browser. The default administrator account is `root` and password is `root`.
|
||||||
|
|
||||||
|
for Developers
|
||||||
|
--------
|
||||||
|
If you want to modify source code and confirm it, you can run GitBucket in auto reloading mode as following:
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\gitbucket> sbt
|
||||||
|
...
|
||||||
|
> container:start
|
||||||
|
...
|
||||||
|
> ~ ;copy-resources;aux-compile
|
||||||
|
```
|
||||||
|
|
||||||
|
Build war file
|
||||||
|
--------
|
||||||
|
|
||||||
|
To build war file, run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\gitbucket> sbt package
|
||||||
|
```
|
||||||
|
|
||||||
|
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
|
||||||
|
|
||||||
|
To build executable war file, run Ant at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact. Please note the current build.xml works on Windows only. Replace `sbt.bat` with `sbt.sh` in build.xml if you want to run it on Linux.
|
||||||
23
doc/notification.md
Normal file
23
doc/notification.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Notification Email
|
||||||
|
========
|
||||||
|
|
||||||
|
GitBucket sends email to target users by enabling the notification email by an administrator.
|
||||||
|
|
||||||
|
The timing of the notification are as follows:
|
||||||
|
|
||||||
|
##### at the issue registration (new issue, new pull request)
|
||||||
|
When a record is saved into the ```ISSUE``` table, GitBucket does the notification.
|
||||||
|
|
||||||
|
##### at the comment registration
|
||||||
|
Among the records in the ```ISSUE_COMMENT``` table, them to be counted as a comment (i.e. the record ```ACTION``` column value is "comment" or "close_comment" or "reopen_comment") are saved, GitBucket does the notification.
|
||||||
|
|
||||||
|
##### at the status update (close, reopen, merge)
|
||||||
|
When the ```CLOSED``` column value is updated, GitBucket does the notification.
|
||||||
|
|
||||||
|
Notified users are as follows:
|
||||||
|
|
||||||
|
* individual repository's owner
|
||||||
|
* collaborators
|
||||||
|
* participants
|
||||||
|
|
||||||
|
However, the operation in person is excluded from the target.
|
||||||
10
doc/readme.md
Normal file
10
doc/readme.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Developer's Guide
|
||||||
|
========
|
||||||
|
* [How to run from source tree](how_to_run.md)
|
||||||
|
* [Directory Structure](directory.md)
|
||||||
|
* [Mapping and Validation](validation.md)
|
||||||
|
* Authentication in Controller (not yet)
|
||||||
|
* [About Action in Issue Comment](comment_action.md)
|
||||||
|
* [Activity Types](activity.md)
|
||||||
|
* [Notification Email](notification.md)
|
||||||
|
* [Automatic Schema Updating](auto_update.md)
|
||||||
71
doc/validation.md
Normal file
71
doc/validation.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
Mapping and Validation
|
||||||
|
========
|
||||||
|
GitBucket uses [scalatra-forms](https://github.com/takezoe/scalatra-forms) to validate request parameters and map them to the scala object. This is inspired by Play2 form mapping / validation.
|
||||||
|
|
||||||
|
At first, define the mapping as following:
|
||||||
|
|
||||||
|
```scala
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
|
case class RegisterForm(name: String, description: String)
|
||||||
|
|
||||||
|
val form = mapping(
|
||||||
|
"name" -> text(required, maxlength(40)),
|
||||||
|
"description" -> text()
|
||||||
|
)(RegisterForm.apply)
|
||||||
|
```
|
||||||
|
|
||||||
|
The servlet have to mixed in ```jp.sf.amateras.scalatra.forms.ClientSideValidationFormSupport``` to validate request parameters and take mapped object. It validates request parameters before action. If any errors are detected, it throws an exception.
|
||||||
|
|
||||||
|
```scala
|
||||||
|
class RegisterServlet extends ScalatraServlet with ClientSideValidationFormSupport {
|
||||||
|
post("/register", form) { form: RegisterForm =>
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the view template, you can add client-side validation by adding ```validate="true"``` to your form. Error messages are set to ```span#error-<fieldname>```.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<form method="POST" action="/register" validate="true">
|
||||||
|
Name: <input type="name" type="text">
|
||||||
|
<span class="error" id="error-name"></span>
|
||||||
|
<br/>
|
||||||
|
Description: <input type="description" type="text">
|
||||||
|
<span class="error" id="error-description"></span>
|
||||||
|
<br/>
|
||||||
|
<input type="submit" value="Register"/>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
Client-side validation calls ```<form-action>/validate``` to validate form contents. It returns a validation result as JSON. In this case, form action is ```/register```, so ```/register/validate``` is called before submitting a form. ```ClientSideValidationFormSupport``` adds this JSON API automatically.
|
||||||
|
|
||||||
|
For Ajax request, you have to use '''ajaxGet''' or '''ajaxPost''' to define action. It almost same as '''get''' or '''post'''. You can implement actions which handle Ajax request as same as normal actions.
|
||||||
|
Small difference is they return validation errors as JSON.
|
||||||
|
|
||||||
|
```scala
|
||||||
|
ajaxPost("/register", form){ form =>
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can call these actions using jQuery as below:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$('#register').click(function(e){
|
||||||
|
$.ajax($(this).attr('action'), {
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
name: $('#name').val(),
|
||||||
|
mail: $('#mail').val()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.done(function(data){
|
||||||
|
$('#result').text('Registered!');
|
||||||
|
})
|
||||||
|
.fail(function(data, status){
|
||||||
|
displayErrors($.parseJSON(data.responseText));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
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.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-continuation-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-http-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-http-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-io-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-io-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-security-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-security-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-server-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-server-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-servlet-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-servlet-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-util-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-util-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-webapp-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-webapp-8.1.16.v20140903.jar
Normal file
Binary file not shown.
BIN
embed-jetty/jetty-xml-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-xml-8.1.16.v20140903.jar
Normal file
Binary file not shown.
11
embed-jetty/update.sh
Executable file
11
embed-jetty/update.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
version=$1
|
||||||
|
output_dir=`dirname $0`
|
||||||
|
git rm -f ${output_dir}/jetty-*.jar
|
||||||
|
for name in 'io' 'servlet' 'xml' 'continuation' 'security' 'util' 'http' 'server' 'webapp'
|
||||||
|
do
|
||||||
|
jar_filename="jetty-${name}-${version}.jar"
|
||||||
|
wget "http://repo1.maven.org/maven2/org/eclipse/jetty/jetty-${name}/${version}/${jar_filename}" -O ${output_dir}/${jar_filename}
|
||||||
|
done
|
||||||
|
git add ${output_dir}/*.jar
|
||||||
|
git commit
|
||||||
9
etc/deploy-assemby-jar.sh
Executable file
9
etc/deploy-assemby-jar.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
mvn deploy:deploy-file \
|
||||||
|
-DgroupId=gitbucket\
|
||||||
|
-DartifactId=gitbucket-assembly\
|
||||||
|
-Dversion=3.0.0\
|
||||||
|
-Dpackaging=jar\
|
||||||
|
-Dfile=../target/scala-2.11/gitbucket-assembly-3.0.0.jar\
|
||||||
|
-DrepositoryId=sourceforge.jp\
|
||||||
|
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
|
||||||
1493
etc/icons.svg
1493
etc/icons.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 78 KiB |
17
etc/pom.xml
Normal file
17
etc/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>jp.sf.amateras</groupId>
|
||||||
|
<artifactId>gitbucket-assembly</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<build>
|
||||||
|
<extensions>
|
||||||
|
<extension>
|
||||||
|
<groupId>org.apache.maven.wagon</groupId>
|
||||||
|
<artifactId>wagon-ssh</artifactId>
|
||||||
|
<version>1.0-beta-6</version>
|
||||||
|
</extension>
|
||||||
|
</extensions>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sbt.version=0.13.8
|
||||||
@@ -1,22 +1,37 @@
|
|||||||
import sbt._
|
import sbt._
|
||||||
import Keys._
|
import Keys._
|
||||||
import org.scalatra.sbt._
|
import org.scalatra.sbt._
|
||||||
import org.scalatra.sbt.PluginKeys._
|
|
||||||
import sbt.ScalaVersion
|
|
||||||
import twirl.sbt.TwirlPlugin._
|
|
||||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
||||||
|
import play.twirl.sbt.SbtTwirl
|
||||||
|
import play.twirl.sbt.Import.TwirlKeys._
|
||||||
|
import sbtassembly._
|
||||||
|
import sbtassembly.AssemblyKeys._
|
||||||
|
|
||||||
object MyBuild extends Build {
|
object MyBuild extends Build {
|
||||||
val Organization = "jp.sf.amateras"
|
val Organization = "gitbucket"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val Version = "0.0.1"
|
val Version = "3.0.0"
|
||||||
val ScalaVersion = "2.10.1"
|
val ScalaVersion = "2.11.6"
|
||||||
val ScalatraVersion = "2.2.1"
|
val ScalatraVersion = "2.3.1"
|
||||||
|
|
||||||
lazy val project = Project (
|
lazy val project = Project (
|
||||||
"gitbucket",
|
"gitbucket",
|
||||||
file("."),
|
file(".")
|
||||||
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
|
)
|
||||||
|
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
|
||||||
|
.settings(
|
||||||
|
test in assembly := {},
|
||||||
|
assemblyMergeStrategy in assembly := {
|
||||||
|
case PathList("META-INF", xs @ _*) =>
|
||||||
|
(xs map {_.toLowerCase}) match {
|
||||||
|
case ("manifest.mf" :: Nil) => MergeStrategy.discard
|
||||||
|
case _ => MergeStrategy.discard
|
||||||
|
}
|
||||||
|
case x => MergeStrategy.first
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.settings(
|
||||||
|
sourcesInBase := false,
|
||||||
organization := Organization,
|
organization := Organization,
|
||||||
name := Name,
|
name := Name,
|
||||||
version := Version,
|
version := Version,
|
||||||
@@ -25,25 +40,39 @@ object MyBuild extends Build {
|
|||||||
Classpaths.typesafeReleases,
|
Classpaths.typesafeReleases,
|
||||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
"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" % "3.0.0.201306101825-r",
|
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.2.201412180340-r",
|
||||||
|
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.2.201412180340-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.11",
|
||||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
|
"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", // 1.4.2 has incompatible APi changes
|
||||||
"org.apache.commons" % "commons-compress" % "1.5",
|
"org.apache.commons" % "commons-compress" % "1.9",
|
||||||
"org.apache.commons" % "commons-email" % "1.3.1",
|
"org.apache.commons" % "commons-email" % "1.3.3",
|
||||||
"com.typesafe.slick" %% "slick" % "1.0.1",
|
"org.apache.httpcomponents" % "httpclient" % "4.3.6",
|
||||||
|
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||||
|
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
"com.h2database" % "h2" % "1.3.171",
|
"com.h2database" % "h2" % "1.4.186",
|
||||||
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
|
// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
|
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "container;provided",
|
||||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
|
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||||
|
"junit" % "junit" % "4.12" % "test",
|
||||||
|
"com.mchange" % "c3p0" % "0.9.5",
|
||||||
|
"com.typesafe" % "config" % "1.2.1",
|
||||||
|
"com.typesafe.play" %% "twirl-compiler" % "1.0.4"
|
||||||
),
|
),
|
||||||
EclipseKeys.withSource := true
|
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
|
||||||
) ++ seq(Twirl.settings: _*)
|
EclipseKeys.withSource := true,
|
||||||
)
|
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
|
||||||
|
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
|
||||||
|
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test",
|
||||||
|
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() ),
|
||||||
|
fork in Test := true,
|
||||||
|
packageOptions += Package.MainClass("JettyLauncher")
|
||||||
|
).enablePlugins(SbtTwirl)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
|
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||||
|
|
||||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
|
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
|
||||||
|
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
|
||||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
|
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
|
||||||
|
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
|
||||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
|
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.8")
|
||||||
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
|
||||||
|
|||||||
Binary file not shown.
BIN
sbt-launch-0.13.8.jar
Normal file
BIN
sbt-launch-0.13.8.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.8.jar" %*
|
||||||
|
|||||||
3
sbt.sh
3
sbt.sh
@@ -1 +1,2 @@
|
|||||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
|
#!/bin/sh
|
||||||
|
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.8.jar "$@"
|
||||||
|
|||||||
91
src/main/java/JettyLauncher.java
Normal file
91
src/main/java/JettyLauncher.java
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||||
|
import org.eclipse.jetty.webapp.WebAppContext;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
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();
|
||||||
|
|
||||||
|
File tmpDir = new File(getGitBucketHome(), "tmp");
|
||||||
|
if(tmpDir.exists()){
|
||||||
|
deleteDirectory(tmpDir);
|
||||||
|
}
|
||||||
|
tmpDir.mkdirs();
|
||||||
|
context.setTempDirectory(tmpDir);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File getGitBucketHome(){
|
||||||
|
String home = System.getProperty("gitbucket.home");
|
||||||
|
if(home != null && home.length() > 0){
|
||||||
|
return new File(home);
|
||||||
|
}
|
||||||
|
home = System.getenv("GITBUCKET_HOME");
|
||||||
|
if(home != null && home.length() > 0){
|
||||||
|
return new File(home);
|
||||||
|
}
|
||||||
|
return new File(System.getProperty("user.home"), ".gitbucket");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteDirectory(File dir){
|
||||||
|
for(File file: dir.listFiles()){
|
||||||
|
if(file.isFile()){
|
||||||
|
file.delete();
|
||||||
|
} else if(file.isDirectory()){
|
||||||
|
deleteDirectory(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/main/java/gitbucket/core/util/PatchUtil.java
Normal file
93
src/main/java/gitbucket/core/util/PatchUtil.java
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package gitbucket.core.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$
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main/resources/database.conf
Normal file
6
src/main/resources/database.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
db {
|
||||||
|
driver = "org.h2.Driver"
|
||||||
|
url = "jdbc:h2:${DatabaseHome};MVCC=true"
|
||||||
|
user = "sa"
|
||||||
|
password = "sa"
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
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;
|
||||||
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;
|
||||||
6
src/main/resources/update/2_3.sql
Normal file
6
src/main/resources/update/2_3.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE PLUGIN (
|
||||||
|
PLUGIN_ID VARCHAR(100) NOT NULL,
|
||||||
|
VERSION VARCHAR(100) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID);
|
||||||
18
src/main/resources/update/2_7.sql
Normal file
18
src/main/resources/update/2_7.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE COMMIT_COMMENT (
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
COMMIT_ID VARCHAR(100) NOT NULL,
|
||||||
|
COMMENT_ID INT AUTO_INCREMENT,
|
||||||
|
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
CONTENT TEXT NOT NULL,
|
||||||
|
FILE_NAME NVARCHAR(100),
|
||||||
|
OLD_LINE_NUMBER INT,
|
||||||
|
NEW_LINE_NUMBER INT,
|
||||||
|
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||||
|
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||||
|
PULL_REQUEST BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, COMMENT_ID);
|
||||||
1
src/main/resources/update/2_8.sql
Normal file
1
src/main/resources/update/2_8.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE COMMIT_COMMENT ALTER COLUMN FILE_NAME NVARCHAR(260);
|
||||||
42
src/main/resources/update/3_1.sql
Normal file
42
src/main/resources/update/3_1.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
DROP TABLE IF EXISTS ACCESS_TOKEN;
|
||||||
|
|
||||||
|
CREATE TABLE ACCESS_TOKEN (
|
||||||
|
ACCESS_TOKEN_ID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
TOKEN_HASH VARCHAR(40) NOT NULL,
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
NOTE TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_PK PRIMARY KEY (ACCESS_TOKEN_ID);
|
||||||
|
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH);
|
||||||
|
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS COMMIT_STATUS;
|
||||||
|
CREATE TABLE COMMIT_STATUS(
|
||||||
|
COMMIT_STATUS_ID INT AUTO_INCREMENT,
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
COMMIT_ID VARCHAR(40) NOT NULL,
|
||||||
|
CONTEXT VARCHAR(255) NOT NULL, -- context is too long (maximum is 255 characters)
|
||||||
|
STATE VARCHAR(10) NOT NULL, -- pending, success, error, or failure
|
||||||
|
TARGET_URL VARCHAR(200),
|
||||||
|
DESCRIPTION TEXT,
|
||||||
|
CREATOR VARCHAR(100) NOT NULL,
|
||||||
|
REGISTERED_DATE TIMESTAMP NOT NULL, -- CREATED_AT
|
||||||
|
UPDATED_DATE TIMESTAMP NOT NULL -- UPDATED_AT
|
||||||
|
);
|
||||||
|
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_PK PRIMARY KEY (COMMIT_STATUS_ID);
|
||||||
|
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_1
|
||||||
|
UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT);
|
||||||
|
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK1
|
||||||
|
FOREIGN KEY (USER_NAME, REPOSITORY_NAME)
|
||||||
|
REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK2
|
||||||
|
FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK3
|
||||||
|
FOREIGN KEY (CREATOR) REFERENCES ACCOUNT (USER_NAME)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -1,17 +1,37 @@
|
|||||||
import app._
|
|
||||||
import org.scalatra._
|
import gitbucket.core.controller._
|
||||||
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
|
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, TransactionFilter}
|
||||||
|
import gitbucket.core.util.Directory
|
||||||
|
|
||||||
|
import java.util.EnumSet
|
||||||
import javax.servlet._
|
import javax.servlet._
|
||||||
|
|
||||||
|
import org.scalatra._
|
||||||
|
|
||||||
|
|
||||||
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("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
||||||
|
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
||||||
|
context.addFilter("accessTokenAuthenticationFilter", new AccessTokenAuthenticationFilter)
|
||||||
|
context.getFilterRegistration("accessTokenAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
|
||||||
|
// Register controllers
|
||||||
|
context.mount(new AnonymousAccessController, "/*")
|
||||||
|
|
||||||
|
PluginRegistry().getControllers.foreach { case (controller, path) =>
|
||||||
|
context.mount(controller, path)
|
||||||
|
}
|
||||||
|
|
||||||
context.mount(new IndexController, "/")
|
context.mount(new IndexController, "/")
|
||||||
context.mount(new SearchController, "/")
|
context.mount(new SearchController, "/")
|
||||||
context.mount(new FileUploadController, "/upload")
|
context.mount(new FileUploadController, "/upload")
|
||||||
context.mount(new SignInController, "/*")
|
|
||||||
context.mount(new DashboardController, "/*")
|
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, "/*")
|
||||||
@@ -21,7 +41,8 @@ class ScalatraBootstrap extends LifeCycle {
|
|||||||
context.mount(new PullRequestsController, "/*")
|
context.mount(new PullRequestsController, "/*")
|
||||||
context.mount(new RepositorySettingsController, "/*")
|
context.mount(new RepositorySettingsController, "/*")
|
||||||
|
|
||||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
// Create GITBUCKET_HOME directory if it does not exist
|
||||||
|
val dir = new java.io.File(Directory.GitBucketHome)
|
||||||
if(!dir.exists){
|
if(!dir.exists){
|
||||||
dir.mkdirs()
|
dir.mkdirs()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import util.{FileUtil, OneselfAuthenticator}
|
|
||||||
import util.StringUtil._
|
|
||||||
import util.Directory._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
import org.scalatra.FlashMapSupport
|
|
||||||
|
|
||||||
class AccountController extends AccountControllerBase
|
|
||||||
with SystemSettingsService with AccountService with RepositoryService with ActivityService
|
|
||||||
with OneselfAuthenticator
|
|
||||||
|
|
||||||
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
|
|
||||||
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
|
|
||||||
with OneselfAuthenticator =>
|
|
||||||
|
|
||||||
case class AccountNewForm(userName: String, password: String,mailAddress: String,
|
|
||||||
url: Option[String], fileId: Option[String])
|
|
||||||
|
|
||||||
case class AccountEditForm(password: Option[String], mailAddress: String,
|
|
||||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
|
||||||
|
|
||||||
val newForm = mapping(
|
|
||||||
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
|
||||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
|
||||||
"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)
|
|
||||||
|
|
||||||
val editForm = mapping(
|
|
||||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
|
||||||
"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)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays user information.
|
|
||||||
*/
|
|
||||||
get("/:userName") {
|
|
||||||
val userName = params("userName")
|
|
||||||
getAccountByUserName(userName).map { account =>
|
|
||||||
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) =>
|
|
||||||
_root_.account.html.members(account, getGroupMembers(account.userName))
|
|
||||||
|
|
||||||
// Repositories
|
|
||||||
case _ =>
|
|
||||||
_root_.account.html.repositories(account,
|
|
||||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
|
||||||
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
|
|
||||||
}
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
val userName = params("userName")
|
|
||||||
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:userName/_edit", editForm)(oneselfOnly { form =>
|
|
||||||
val userName = params("userName")
|
|
||||||
getAccountByUserName(userName).map { account =>
|
|
||||||
updateAccount(account.copy(
|
|
||||||
password = form.password.map(sha1).getOrElse(account.password),
|
|
||||||
mailAddress = form.mailAddress,
|
|
||||||
url = form.url))
|
|
||||||
|
|
||||||
updateImage(userName, form.fileId, form.clearImage)
|
|
||||||
flash += "info" -> "Account information has been updated."
|
|
||||||
redirect(s"/${userName}/_edit")
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/register"){
|
|
||||||
if(loadSystemSettings().allowAccountRegistration){
|
|
||||||
if(context.loginAccount.isDefined){
|
|
||||||
redirect("/")
|
|
||||||
} else {
|
|
||||||
account.html.edit(None, None)
|
|
||||||
}
|
|
||||||
} else NotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
post("/register", newForm){ form =>
|
|
||||||
if(loadSystemSettings().allowAccountRegistration){
|
|
||||||
createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url)
|
|
||||||
updateImage(form.userName, form.fileId, false)
|
|
||||||
redirect("/signin")
|
|
||||||
} else NotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import _root_.util.Directory._
|
|
||||||
import _root_.util.{FileUtil, Validations}
|
|
||||||
import org.scalatra._
|
|
||||||
import org.scalatra.json._
|
|
||||||
import org.json4s._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import model.Account
|
|
||||||
import scala.Some
|
|
||||||
import service.AccountService
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides generic features for controller implementations.
|
|
||||||
*/
|
|
||||||
abstract class ControllerBase extends ScalatraFilter
|
|
||||||
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
|
|
||||||
|
|
||||||
implicit val jsonFormats = DefaultFormats
|
|
||||||
|
|
||||||
// Don't set content type via Accept header.
|
|
||||||
override def format(implicit request: HttpServletRequest) = ""
|
|
||||||
|
|
||||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
|
||||||
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("LOGIN_ACCOUNT").asInstanceOf[Account]
|
|
||||||
if(account == null){
|
|
||||||
// Redirect to login form
|
|
||||||
httpResponse.sendRedirect(context + "/signin?" + path)
|
|
||||||
} else if(account.isAdmin){
|
|
||||||
// H2 Console (administrators only)
|
|
||||||
chain.doFilter(request, response)
|
|
||||||
} else {
|
|
||||||
// Redirect to dashboard
|
|
||||||
httpResponse.sendRedirect(context + "/")
|
|
||||||
}
|
|
||||||
} else if(path.startsWith("/git/")){
|
|
||||||
// Git repository
|
|
||||||
chain.doFilter(request, response)
|
|
||||||
} else {
|
|
||||||
// Scalatra actions
|
|
||||||
super.doFilter(request, response, chain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the context object for the request.
|
|
||||||
*/
|
|
||||||
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
|
|
||||||
|
|
||||||
private def currentURL: String = {
|
|
||||||
val queryString = request.getQueryString
|
|
||||||
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def LoginAccount: Option[Account] = {
|
|
||||||
session.get("LOGIN_ACCOUNT") match {
|
|
||||||
case Some(x: Account) => Some(x)
|
|
||||||
case _ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def ajaxGet(path : String)(action : => Any) : Route = {
|
|
||||||
super.get(path){
|
|
||||||
request.setAttribute("AJAX", "true")
|
|
||||||
action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
|
|
||||||
super.ajaxGet(path, form){ form =>
|
|
||||||
request.setAttribute("AJAX", "true")
|
|
||||||
action(form)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def ajaxPost(path : String)(action : => Any) : Route = {
|
|
||||||
super.post(path){
|
|
||||||
request.setAttribute("AJAX", "true")
|
|
||||||
action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
|
|
||||||
super.ajaxPost(path, form){ form =>
|
|
||||||
request.setAttribute("AJAX", "true")
|
|
||||||
action(form)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected def NotFound() = {
|
|
||||||
if(request.getAttribute("AJAX") == null){
|
|
||||||
org.scalatra.NotFound(html.error("Not Found"))
|
|
||||||
} else {
|
|
||||||
org.scalatra.NotFound()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected def Unauthorized()(implicit context: app.Context) = {
|
|
||||||
if(request.getAttribute("AJAX") == null){
|
|
||||||
if(context.loginAccount.isDefined){
|
|
||||||
org.scalatra.Unauthorized(redirect("/"))
|
|
||||||
} else {
|
|
||||||
if(request.getMethod.toUpperCase == "POST"){
|
|
||||||
org.scalatra.Unauthorized(redirect("/signin"))
|
|
||||||
} else {
|
|
||||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
org.scalatra.Unauthorized()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected def baseUrl = {
|
|
||||||
val url = request.getRequestURL.toString
|
|
||||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context object for the current request.
|
|
||||||
*/
|
|
||||||
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
|
|
||||||
|
|
||||||
def redirectUrl = {
|
|
||||||
if(request.getParameter("redirect") != null){
|
|
||||||
request.getParameter("redirect")
|
|
||||||
} else {
|
|
||||||
currentUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = {
|
|
||||||
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse {
|
|
||||||
val newObject = action
|
|
||||||
request.setAttribute("cache." + key, newObject)
|
|
||||||
newObject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base trait for controllers which manages account information.
|
|
||||||
*/
|
|
||||||
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
|
|
||||||
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(getUploadedFilename(fileId).get)
|
|
||||||
FileUtils.moveFile(
|
|
||||||
getTemporaryFile(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): Option[String] =
|
|
||||||
getAccountByUserName(value).map { _ => "User already exists." }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
|
||||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
|
||||||
getAccountByMailAddress(value)
|
|
||||||
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
|
||||||
.map { _ => "Mail address is already registered." }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base trait for controllers which needs file uploading feature.
|
|
||||||
*/
|
|
||||||
trait FileUploadControllerBase {
|
|
||||||
|
|
||||||
def generateFileId: String =
|
|
||||||
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
|
|
||||||
|
|
||||||
def TemporaryDir(implicit session: HttpSession): java.io.File =
|
|
||||||
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
|
|
||||||
|
|
||||||
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
|
|
||||||
new java.io.File(TemporaryDir, fileId)
|
|
||||||
|
|
||||||
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
|
|
||||||
// getTemporaryFile(fileId).delete()
|
|
||||||
|
|
||||||
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
|
|
||||||
FileUtils.deleteDirectory(TemporaryDir)
|
|
||||||
|
|
||||||
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
|
|
||||||
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
|
|
||||||
if(filename.isDefined){
|
|
||||||
session.removeAttribute("upload_" + fileId)
|
|
||||||
}
|
|
||||||
filename
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import util.Directory._
|
|
||||||
import util._
|
|
||||||
import service._
|
|
||||||
import java.io.File
|
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import org.apache.commons.io._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
import org.eclipse.jgit.lib.PersonIdent
|
|
||||||
import scala.Some
|
|
||||||
|
|
||||||
class CreateRepositoryController extends CreateRepositoryControllerBase
|
|
||||||
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
|
||||||
with UsersAuthenticator with ReadableUsersAuthenticator
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new repository.
|
|
||||||
*/
|
|
||||||
trait CreateRepositoryControllerBase extends ControllerBase {
|
|
||||||
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
|
||||||
with UsersAuthenticator with ReadableUsersAuthenticator =>
|
|
||||||
|
|
||||||
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
|
||||||
|
|
||||||
case class ForkRepositoryForm(owner: String, name: String)
|
|
||||||
|
|
||||||
val newForm = mapping(
|
|
||||||
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
|
|
||||||
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
|
|
||||||
"description" -> trim(label("Description" , optional(text()))),
|
|
||||||
"isPrivate" -> trim(label("Repository Type", boolean())),
|
|
||||||
"createReadme" -> trim(label("Create README" , boolean()))
|
|
||||||
)(RepositoryCreationForm.apply)
|
|
||||||
|
|
||||||
val forkForm = mapping(
|
|
||||||
"owner" -> trim(label("Repository owner", text(required))),
|
|
||||||
"name" -> trim(label("Repository name", text(required)))
|
|
||||||
)(ForkRepositoryForm.apply)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the new repository form.
|
|
||||||
*/
|
|
||||||
get("/new")(usersOnly {
|
|
||||||
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new repository.
|
|
||||||
*/
|
|
||||||
post("/new", newForm)(usersOnly { form =>
|
|
||||||
LockUtil.lock(s"${form.owner}/${form.name}/create"){
|
|
||||||
if(getRepository(form.owner, form.name, 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 { userName =>
|
|
||||||
addCollaborator(form.owner, form.name, 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){
|
|
||||||
val tmpdir = getInitRepositoryDir(form.owner, 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
|
|
||||||
.setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress))
|
|
||||||
.setMessage("Initial commit").call
|
|
||||||
git.push.call
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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}/create"){
|
|
||||||
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
|
|
||||||
// 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))
|
|
||||||
|
|
||||||
// insert commit id
|
|
||||||
JGitUtil.withGit(getRepositoryDir(loginUserName, repository.name)){ git =>
|
|
||||||
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
|
|
||||||
JGitUtil.getCommitLog(git, branch) match {
|
|
||||||
case Right((commits, _)) => commits.foreach { commit =>
|
|
||||||
if(!existsCommitId(loginUserName, repository.name, commit.id)){
|
|
||||||
insertCommitId(loginUserName, repository.name, commit.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case Left(_) => ???
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record activity
|
|
||||||
recordForkActivity(repository.owner, repository.name, loginUserName)
|
|
||||||
}
|
|
||||||
// redirect to the repository
|
|
||||||
redirect("/%s/%s".format(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): Option[String] =
|
|
||||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Duplicate check for the repository name.
|
|
||||||
*/
|
|
||||||
private def unique: Constraint = new Constraint(){
|
|
||||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
|
||||||
params.get("owner").flatMap { userName =>
|
|
||||||
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import util.UsersAuthenticator
|
|
||||||
|
|
||||||
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 sessionKey = "dashboard/issues"
|
|
||||||
val condition = if(request.getQueryString == null)
|
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
|
||||||
else IssueSearchCondition(request)
|
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
val repositories = getUserRepositories(userName, baseUrl).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, repositories: _*),
|
|
||||||
page,
|
|
||||||
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
|
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
|
|
||||||
condition),
|
|
||||||
countIssue(condition, Map.empty, false, repositories: _*),
|
|
||||||
countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
|
|
||||||
countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
|
|
||||||
countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
|
|
||||||
condition,
|
|
||||||
filter)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private def searchPullRequests(filter: String, repository: Option[String]) = {
|
|
||||||
import IssuesService._
|
|
||||||
import PullRequestService._
|
|
||||||
|
|
||||||
// condition
|
|
||||||
val sessionKey = "dashboard/pulls"
|
|
||||||
val condition = {
|
|
||||||
if(request.getQueryString == null)
|
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
|
||||||
else
|
|
||||||
IssueSearchCondition(request)
|
|
||||||
}.copy(repo = repository)
|
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
val repositories = getUserRepositories(userName, baseUrl).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, repositories: _*)
|
|
||||||
|
|
||||||
getRepositoryNamesOfUser(userName).map { repoName =>
|
|
||||||
(userName, repoName, counts.collectFirst { case (_, repoName, count) => count })
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboard.html.pulls(
|
|
||||||
pulls.html.listparts(
|
|
||||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
|
|
||||||
page,
|
|
||||||
countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
|
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
|
|
||||||
condition,
|
|
||||||
None,
|
|
||||||
false),
|
|
||||||
getPullRequestCountGroupByUser(condition.state == "closed", userName, None),
|
|
||||||
getRepositoryNamesOfUser(userName).map { RepoName =>
|
|
||||||
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
|
|
||||||
}.sortBy(_._3).reverse,
|
|
||||||
condition,
|
|
||||||
filter)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import util.{FileUtil}
|
|
||||||
import org.scalatra._
|
|
||||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides Ajax based file upload functionality.
|
|
||||||
*
|
|
||||||
* This servlet saves uploaded file as temporary file and returns the unique id.
|
|
||||||
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
|
|
||||||
*/
|
|
||||||
class FileUploadController extends ScalatraServlet
|
|
||||||
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
|
|
||||||
|
|
||||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
|
||||||
|
|
||||||
post("/image"){
|
|
||||||
fileParams.get("file") match {
|
|
||||||
case Some(file) if(FileUtil.isImage(file.name)) => {
|
|
||||||
val fileId = generateFileId
|
|
||||||
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
|
|
||||||
session += "upload_" + fileId -> file.name
|
|
||||||
Ok(fileId)
|
|
||||||
}
|
|
||||||
case None => BadRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import util._
|
|
||||||
import service._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
class IndexController extends IndexControllerBase
|
|
||||||
with RepositoryService with SystemSettingsService with ActivityService with AccountService
|
|
||||||
with UsersAuthenticator
|
|
||||||
|
|
||||||
trait IndexControllerBase extends ControllerBase {
|
|
||||||
self: RepositoryService with SystemSettingsService with ActivityService with AccountService
|
|
||||||
with UsersAuthenticator =>
|
|
||||||
|
|
||||||
get("/"){
|
|
||||||
val loginAccount = context.loginAccount
|
|
||||||
|
|
||||||
html.index(getRecentActivities(),
|
|
||||||
getVisibleRepositories(loginAccount, baseUrl),
|
|
||||||
loadSystemSettings(),
|
|
||||||
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON API for collaborator completion.
|
|
||||||
*
|
|
||||||
* TODO Move to other controller?
|
|
||||||
*/
|
|
||||||
get("/_user/proposals")(usersOnly {
|
|
||||||
contentType = formats("json")
|
|
||||||
org.json4s.jackson.Serialization.write(
|
|
||||||
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import IssuesService._
|
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier}
|
|
||||||
import org.scalatra.Ok
|
|
||||||
|
|
||||||
class IssuesController extends IssuesControllerBase
|
|
||||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
|
||||||
|
|
||||||
trait IssuesControllerBase extends ControllerBase {
|
|
||||||
self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
|
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
|
||||||
|
|
||||||
case class IssueCreateForm(title: String, content: Option[String],
|
|
||||||
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
|
|
||||||
case class IssueEditForm(title: String, content: Option[String])
|
|
||||||
case class CommentForm(issueId: Int, content: String)
|
|
||||||
case class IssueStateForm(issueId: Int, content: Option[String])
|
|
||||||
|
|
||||||
val issueCreateForm = mapping(
|
|
||||||
"title" -> trim(label("Title", text(required))),
|
|
||||||
"content" -> trim(optional(text())),
|
|
||||||
"assignedUserName" -> trim(optional(text())),
|
|
||||||
"milestoneId" -> trim(optional(number())),
|
|
||||||
"labelNames" -> trim(optional(text()))
|
|
||||||
)(IssueCreateForm.apply)
|
|
||||||
|
|
||||||
val issueEditForm = mapping(
|
|
||||||
"title" -> trim(label("Title", text(required))),
|
|
||||||
"content" -> trim(optional(text()))
|
|
||||||
)(IssueEditForm.apply)
|
|
||||||
|
|
||||||
val commentForm = mapping(
|
|
||||||
"issueId" -> label("Issue Id", number()),
|
|
||||||
"content" -> trim(label("Comment", text(required)))
|
|
||||||
)(CommentForm.apply)
|
|
||||||
|
|
||||||
val issueStateForm = mapping(
|
|
||||||
"issueId" -> label("Issue Id", number()),
|
|
||||||
"content" -> trim(optional(text()))
|
|
||||||
)(IssueStateForm.apply)
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues")(referrersOnly {
|
|
||||||
searchIssues("all", _)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
|
|
||||||
searchIssues("assigned", _)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
|
|
||||||
searchIssues("created_by", _)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
val issueId = params("id")
|
|
||||||
|
|
||||||
getIssue(owner, name, issueId) map {
|
|
||||||
issues.html.issue(
|
|
||||||
_,
|
|
||||||
getComments(owner, name, issueId.toInt),
|
|
||||||
getIssueLabels(owner, name, issueId.toInt),
|
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
|
||||||
getMilestonesWithIssueCount(owner, name),
|
|
||||||
getLabels(owner, name),
|
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
|
||||||
repository)
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
|
|
||||||
issues.html.create(
|
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
|
||||||
getMilestones(owner, name),
|
|
||||||
getLabels(owner, name),
|
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
|
||||||
repository)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
val writable = hasWritePermission(owner, name, context.loginAccount)
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
|
|
||||||
// insert issue
|
|
||||||
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
|
||||||
if(writable) form.assignedUserName else None,
|
|
||||||
if(writable) form.milestoneId else None)
|
|
||||||
|
|
||||||
// insert labels
|
|
||||||
if(writable){
|
|
||||||
form.labelNames.map { value =>
|
|
||||||
val labels = getLabels(owner, name)
|
|
||||||
value.split(",").foreach { labelName =>
|
|
||||||
labels.find(_.labelName == labelName).map { label =>
|
|
||||||
registerIssueLabel(owner, name, issueId, label.labelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// record activity
|
|
||||||
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
|
||||||
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
|
|
||||||
getIssue(owner, name, params("id")).map { issue =>
|
|
||||||
if(isEditable(owner, name, issue.openedUserName)){
|
|
||||||
updateIssue(owner, name, issue.issueId, form.title, form.content)
|
|
||||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
|
||||||
} else Unauthorized
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
|
||||||
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/${
|
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
|
||||||
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/${
|
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
|
|
||||||
getComment(owner, name, params("id")).map { comment =>
|
|
||||||
if(isEditable(owner, name, comment.commentedUserName)){
|
|
||||||
updateComment(comment.commentId, form.content)
|
|
||||||
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
|
||||||
} else Unauthorized
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
|
|
||||||
getIssue(repository.owner, repository.name, params("id")) map { x =>
|
|
||||||
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
|
|
||||||
params.get("dataType") collect {
|
|
||||||
case t if t == "html" => issues.html.editissue(
|
|
||||||
x.title, x.content, x.issueId, x.userName, x.repositoryName)
|
|
||||||
} getOrElse {
|
|
||||||
contentType = formats("json")
|
|
||||||
org.json4s.jackson.Serialization.write(
|
|
||||||
Map("title" -> x.title,
|
|
||||||
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
|
|
||||||
repository, false, true)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else Unauthorized
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
|
|
||||||
getComment(repository.owner, repository.name, params("id")) map { x =>
|
|
||||||
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
|
||||||
params.get("dataType") collect {
|
|
||||||
case t if t == "html" => issues.html.editcomment(
|
|
||||||
x.content, x.commentId, x.userName, x.repositoryName)
|
|
||||||
} getOrElse {
|
|
||||||
contentType = formats("json")
|
|
||||||
org.json4s.jackson.Serialization.write(
|
|
||||||
Map("content" -> view.Markdown.toHtml(x.content,
|
|
||||||
repository, false, true)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else Unauthorized
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
|
||||||
val issueId = params("id").toInt
|
|
||||||
|
|
||||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
|
||||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
|
||||||
val issueId = params("id").toInt
|
|
||||||
|
|
||||||
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
|
||||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
|
||||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
|
|
||||||
Ok("updated")
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
|
|
||||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
|
|
||||||
milestoneId("milestoneId").map { milestoneId =>
|
|
||||||
getMilestonesWithIssueCount(repository.owner, repository.name)
|
|
||||||
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
|
|
||||||
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
|
|
||||||
} getOrElse NotFound
|
|
||||||
} getOrElse Ok()
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
|
||||||
val action = params.get("value")
|
|
||||||
|
|
||||||
executeBatch(repository) {
|
|
||||||
handleComment(_, None, repository)( _ => action)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
|
|
||||||
val labelId = params("value").toInt
|
|
||||||
|
|
||||||
executeBatch(repository) { issueId =>
|
|
||||||
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
|
||||||
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
|
|
||||||
val value = assignedUserName("value")
|
|
||||||
|
|
||||||
executeBatch(repository) {
|
|
||||||
updateAssignedUserName(repository.owner, repository.name, _, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
|
|
||||||
val value = milestoneId("value")
|
|
||||||
|
|
||||||
executeBatch(repository) {
|
|
||||||
updateMilestoneId(repository.owner, repository.name, _, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
|
||||||
val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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))) = {
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.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) )
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
Notifier() match {
|
|
||||||
case f =>
|
|
||||||
content foreach {
|
|
||||||
f.toNotify(repository, issueId, _){
|
|
||||||
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
|
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
action foreach {
|
|
||||||
f.toNotify(repository, issueId, _){
|
|
||||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
issue -> commentId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
|
||||||
val owner = repository.owner
|
|
||||||
val repoName = repository.name
|
|
||||||
val filterUser = Map(filter -> params.getOrElse("userName", ""))
|
|
||||||
val page = IssueSearchCondition.page(request)
|
|
||||||
val sessionKey = s"${owner}/${repoName}/issues"
|
|
||||||
|
|
||||||
// retrieve search condition
|
|
||||||
val condition = if(request.getQueryString == null){
|
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
|
||||||
} else IssueSearchCondition(request)
|
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
import service._
|
|
||||||
import util.CollaboratorsAuthenticator
|
|
||||||
|
|
||||||
class LabelsController extends LabelsControllerBase
|
|
||||||
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
|
|
||||||
|
|
||||||
trait LabelsControllerBase extends ControllerBase {
|
|
||||||
self: LabelsService with RepositoryService with CollaboratorsAuthenticator =>
|
|
||||||
|
|
||||||
case class LabelForm(labelName: String, color: String)
|
|
||||||
|
|
||||||
val newForm = mapping(
|
|
||||||
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
|
||||||
"newColor" -> trim(label("Color", text(required, color)))
|
|
||||||
)(LabelForm.apply)
|
|
||||||
|
|
||||||
val editForm = mapping(
|
|
||||||
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
|
||||||
"editColor" -> trim(label("Color", text(required, color)))
|
|
||||||
)(LabelForm.apply)
|
|
||||||
|
|
||||||
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
|
|
||||||
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
|
|
||||||
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository =>
|
|
||||||
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
|
||||||
issues.labels.html.edit(Some(label), repository)
|
|
||||||
} getOrElse NotFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) =>
|
|
||||||
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
|
|
||||||
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository =>
|
|
||||||
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
|
||||||
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): Option[String] =
|
|
||||||
if(!value.matches("^[^,]+$")){
|
|
||||||
Some(s"${name} contains invalid character.")
|
|
||||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
|
||||||
Some(s"${name} starts with invalid character.")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier}
|
|
||||||
import util.Directory._
|
|
||||||
import util.Implicits._
|
|
||||||
import service._
|
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
import org.eclipse.jgit.transport.RefSpec
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import scala.collection.JavaConverters._
|
|
||||||
import org.eclipse.jgit.lib.PersonIdent
|
|
||||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
|
|
||||||
import service.IssuesService._
|
|
||||||
import service.PullRequestService._
|
|
||||||
import util.JGitUtil.DiffInfo
|
|
||||||
import scala.Some
|
|
||||||
import service.RepositoryService.RepositoryTreeNode
|
|
||||||
import util.JGitUtil.CommitInfo
|
|
||||||
|
|
||||||
class PullRequestsController extends PullRequestsControllerBase
|
|
||||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
|
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
|
||||||
|
|
||||||
trait PullRequestsControllerBase extends ControllerBase {
|
|
||||||
self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
|
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
|
||||||
|
|
||||||
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))),
|
|
||||||
"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,
|
|
||||||
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 =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
val issueId = params("id").toInt
|
|
||||||
|
|
||||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
|
||||||
JGitUtil.withGit(getRepositoryDir(owner, name)){ git =>
|
|
||||||
val requestCommitId = git.getRepository.resolve(pullreq.requestBranch)
|
|
||||||
|
|
||||||
val (commits, diffs) =
|
|
||||||
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
|
||||||
|
|
||||||
pulls.html.pullreq(
|
|
||||||
issue, pullreq,
|
|
||||||
getComments(owner, name, issueId.toInt),
|
|
||||||
(getCollaborators(owner, name) :+ owner).sorted,
|
|
||||||
getMilestonesWithIssueCount(owner, name),
|
|
||||||
commits,
|
|
||||||
diffs,
|
|
||||||
if(issue.closed){
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch)
|
|
||||||
},
|
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
|
||||||
repository,
|
|
||||||
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
|
||||||
}
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
|
||||||
LockUtil.lock(s"${repository.owner}/${repository.name}/merge"){
|
|
||||||
val issueId = params("id").toInt
|
|
||||||
|
|
||||||
getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) =>
|
|
||||||
val remote = getRepositoryDir(repository.owner, repository.name)
|
|
||||||
val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
|
|
||||||
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
|
|
||||||
|
|
||||||
try {
|
|
||||||
// mark issue as merged and close.
|
|
||||||
val loginAccount = context.loginAccount.get
|
|
||||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, form.message, "merge")
|
|
||||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close")
|
|
||||||
updateClosed(repository.owner, repository.name, issueId, true)
|
|
||||||
|
|
||||||
// record activity
|
|
||||||
recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message)
|
|
||||||
|
|
||||||
// TODO apply ref comment
|
|
||||||
|
|
||||||
// fetch pull request to temporary working repository
|
|
||||||
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
|
|
||||||
|
|
||||||
git.fetch
|
|
||||||
.setRemote(getRepositoryDir(repository.owner, repository.name).toURI.toString)
|
|
||||||
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
|
|
||||||
|
|
||||||
// merge pull request
|
|
||||||
git.checkout.setName(pullreq.branch).call
|
|
||||||
|
|
||||||
val result = git.merge
|
|
||||||
.include(git.getRepository.resolve(pullRequestBranchName))
|
|
||||||
.setFastForward(FastForwardMode.NO_FF)
|
|
||||||
.setCommit(false)
|
|
||||||
.call
|
|
||||||
|
|
||||||
if(result.getConflicts != null){
|
|
||||||
throw new RuntimeException("This pull request can't merge automatically.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge commit
|
|
||||||
git.getRepository.writeMergeCommitMsg(
|
|
||||||
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
|
|
||||||
+ form.message)
|
|
||||||
|
|
||||||
git.commit
|
|
||||||
.setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
|
|
||||||
.call
|
|
||||||
|
|
||||||
// push
|
|
||||||
git.push.call
|
|
||||||
|
|
||||||
val (commits, _) = getRequestCompareInfo(repository.owner, repository.name, pullreq.commitIdFrom,
|
|
||||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
|
||||||
|
|
||||||
commits.flatten.foreach { commit =>
|
|
||||||
if(!existsCommitId(repository.owner, repository.name, commit.id)){
|
|
||||||
insertCommitId(repository.owner, repository.name, commit.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
Notifier().toNotify(repository, issueId, "merge"){
|
|
||||||
Notifier.msgStatus(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
git.getRepository.close
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = {
|
|
||||||
// TODO Are there more quick way?
|
|
||||||
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
|
|
||||||
val remote = getRepositoryDir(userName, repositoryName)
|
|
||||||
val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")
|
|
||||||
if(tmpdir.exists()){
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call
|
|
||||||
try {
|
|
||||||
git.checkout.setName(branch).call
|
|
||||||
|
|
||||||
git.fetch
|
|
||||||
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
|
|
||||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
|
|
||||||
|
|
||||||
val result = git.merge
|
|
||||||
.include(git.getRepository.resolve("FETCH_HEAD"))
|
|
||||||
.setCommit(false).call
|
|
||||||
|
|
||||||
result.getConflicts != null
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
git.getRepository.close
|
|
||||||
FileUtils.deleteDirectory(tmpdir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
|
||||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
|
||||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
|
||||||
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
|
|
||||||
withGit(
|
|
||||||
getRepositoryDir(originUserName, originRepositoryName),
|
|
||||||
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"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
|
||||||
}
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
case _ => {
|
|
||||||
JGitUtil.withGit(getRepositoryDir(forkedRepository.owner, forkedRepository.name)){ git =>
|
|
||||||
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
|
|
||||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
|
|
||||||
val Seq(origin, forked) = multiParams("splat")
|
|
||||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
|
|
||||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
|
|
||||||
|
|
||||||
(getRepository(originOwner, repository.name, baseUrl),
|
|
||||||
getRepository(forkedOwner, repository.name, baseUrl)) match {
|
|
||||||
case (Some(originRepository), Some(forkedRepository)) => {
|
|
||||||
withGit(
|
|
||||||
getRepositoryDir(originOwner, repository.name),
|
|
||||||
getRepositoryDir(forkedOwner, repository.name)
|
|
||||||
){ case (oldGit, newGit) =>
|
|
||||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
|
||||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
|
||||||
|
|
||||||
val forkedId = getForkedCommitId(oldGit, newGit,
|
|
||||||
originOwner, repository.name, originBranch,
|
|
||||||
forkedOwner, repository.name, forkedBranch)
|
|
||||||
|
|
||||||
val oldId = oldGit.getRepository.resolve(forkedId)
|
|
||||||
val newId = newGit.getRepository.resolve(forkedBranch)
|
|
||||||
|
|
||||||
val (commits, diffs) = getRequestCompareInfo(
|
|
||||||
originOwner, repository.name, oldId.getName,
|
|
||||||
forkedOwner, repository.name, newId.getName)
|
|
||||||
|
|
||||||
pulls.html.compare(
|
|
||||||
commits,
|
|
||||||
diffs,
|
|
||||||
repository.repository.originUserName.map { userName =>
|
|
||||||
userName :: getForkedRepositories(userName, repository.name)
|
|
||||||
} getOrElse List(repository.owner),
|
|
||||||
originBranch,
|
|
||||||
forkedBranch,
|
|
||||||
oldId.getName,
|
|
||||||
newId.getName,
|
|
||||||
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch),
|
|
||||||
repository,
|
|
||||||
originRepository,
|
|
||||||
forkedRepository,
|
|
||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case _ => 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 = repository.name,
|
|
||||||
requestBranch = form.requestBranch,
|
|
||||||
commitIdFrom = form.commitIdFrom,
|
|
||||||
commitIdTo = form.commitIdTo)
|
|
||||||
|
|
||||||
// fetch requested branch
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
git.fetch
|
|
||||||
.setRemote(getRepositoryDir(form.requestUserName, repository.name).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"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles w Git object simultaneously.
|
|
||||||
*/
|
|
||||||
private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = {
|
|
||||||
val oldGit = Git.open(oldDir)
|
|
||||||
val newGit = Git.open(newDir)
|
|
||||||
try {
|
|
||||||
action(oldGit, newGit)
|
|
||||||
} finally {
|
|
||||||
oldGit.getRepository.close
|
|
||||||
newGit.getRepository.close
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
|
|
||||||
*/
|
|
||||||
private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
|
|
||||||
node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
|
|
||||||
*/
|
|
||||||
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
|
|
||||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
|
|
||||||
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
|
|
||||||
existsCommitId(userName, repositoryName, commit.getName) &&
|
|
||||||
JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
|
|
||||||
}.head.id
|
|
||||||
|
|
||||||
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
|
||||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
|
|
||||||
|
|
||||||
withGit(
|
|
||||||
getRepositoryDir(userName, repositoryName),
|
|
||||||
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.time) == view.helpers.date(commit2.time)
|
|
||||||
}
|
|
||||||
|
|
||||||
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
|
|
||||||
|
|
||||||
(commits, diffs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = {
|
|
||||||
val owner = repository.owner
|
|
||||||
val repoName = repository.name
|
|
||||||
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
|
|
||||||
val page = IssueSearchCondition.page(request)
|
|
||||||
val sessionKey = s"${owner}/${repoName}/pulls"
|
|
||||||
|
|
||||||
// retrieve search condition
|
|
||||||
val condition = if(request.getQueryString == null){
|
|
||||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
|
||||||
} else IssueSearchCondition(request)
|
|
||||||
|
|
||||||
session.put(sessionKey, condition)
|
|
||||||
|
|
||||||
pulls.html.list(
|
|
||||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
|
||||||
getPullRequestCountGroupByUser(condition.state == "closed", 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,126 +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
|
|
||||||
import org.scalatra.FlashMapSupport
|
|
||||||
|
|
||||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
|
||||||
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
|
|
||||||
|
|
||||||
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
|
|
||||||
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"/${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,
|
|
||||||
form.defaultBranch,
|
|
||||||
repository.repository.parentUserName.map { _ =>
|
|
||||||
repository.repository.isPrivate
|
|
||||||
} getOrElse form.isPrivate
|
|
||||||
)
|
|
||||||
flash += "info" -> "Repository settings has been updated."
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/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 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"/${repository.owner}")
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides Constraint to validate the collaborator name.
|
|
||||||
*/
|
|
||||||
private def collaborator: Constraint = new Constraint(){
|
|
||||||
override def validate(name: String, value: String): Option[String] = {
|
|
||||||
val paths = request.getRequestURI.split("/")
|
|
||||||
getAccountByUserName(value) match {
|
|
||||||
case None => Some("User does not exist.")
|
|
||||||
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName))
|
|
||||||
=> Some("User can access this repository already.")
|
|
||||||
case _ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import util.Directory._
|
|
||||||
import util.Implicits._
|
|
||||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
|
|
||||||
import service._
|
|
||||||
import org.scalatra._
|
|
||||||
import java.io.File
|
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import org.eclipse.jgit.lib._
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import org.eclipse.jgit.treewalk._
|
|
||||||
|
|
||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
|
||||||
with RepositoryService with AccountService with ReferrerAuthenticator
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The repository viewer.
|
|
||||||
*/
|
|
||||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
|
||||||
self: RepositoryService with AccountService with ReferrerAuthenticator =>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns converted HTML from Markdown for preview.
|
|
||||||
*/
|
|
||||||
post("/:owner/:repository/_preview")(referrersOnly { repository =>
|
|
||||||
contentType = "text/html"
|
|
||||||
view.helpers.markdown(params("content"), repository,
|
|
||||||
params("enableWikiLink").toBoolean,
|
|
||||||
params("enableRefsLink").toBoolean)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the file list of the repository root and the default branch.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository")(referrersOnly {
|
|
||||||
fileList(_)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the file list of the specified path and branch.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
|
|
||||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
|
||||||
if(path.isEmpty){
|
|
||||||
fileList(repository, id)
|
|
||||||
} else {
|
|
||||||
fileList(repository, id, path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the commit list of the specified resource.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
|
|
||||||
val (branchName, path) = splitPath(repository, multiParams("splat").head)
|
|
||||||
val page = params.getOrElse("page", "1").toInt
|
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
|
||||||
case Right((logs, hasNext)) =>
|
|
||||||
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
|
||||||
logs.splitWith{ (commit1, commit2) =>
|
|
||||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
|
||||||
}, page, hasNext)
|
|
||||||
case Left(_) => NotFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the file content of the specified branch or commit.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
|
||||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
|
||||||
val raw = params.get("raw").getOrElse("false").toBoolean
|
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
|
||||||
|
|
||||||
@scala.annotation.tailrec
|
|
||||||
def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match {
|
|
||||||
case true if(walk.getPathString == path) => walk.getObjectId(0)
|
|
||||||
case true => getPathObjectId(path, walk)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(StringUtil.convertFromByteArray))
|
|
||||||
} else {
|
|
||||||
// binary
|
|
||||||
JGitUtil.ContentInfo("binary", None)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// image or large
|
|
||||||
JGitUtil.ContentInfo(viewer, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays details of the specified commit.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
|
||||||
val id = params("id")
|
|
||||||
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
|
||||||
|
|
||||||
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
|
||||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
|
||||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
|
||||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
|
||||||
repository, diffs, oldCommitId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays branches.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
|
||||||
JGitUtil.withGit(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, repository)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays tags.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/tags")(referrersOnly {
|
|
||||||
repo.html.tags(_)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download repository contents as an archive.
|
|
||||||
*/
|
|
||||||
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
|
|
||||||
val name = params("name")
|
|
||||||
|
|
||||||
if(name.endsWith(".zip")){
|
|
||||||
val revision = name.replaceFirst("\\.zip$", "")
|
|
||||||
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
|
||||||
if(workDir.exists){
|
|
||||||
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),
|
|
||||||
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
|
|
||||||
} orElse Some(path.split("/")(0)) get
|
|
||||||
|
|
||||||
(id, path.substring(id.length).replaceFirst("^/", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides HTML of the file list.
|
|
||||||
*
|
|
||||||
* @param repository the repository information
|
|
||||||
* @param revstr the branch name or commit id(optional)
|
|
||||||
* @param path the directory path (optional)
|
|
||||||
* @return HTML of the file list
|
|
||||||
*/
|
|
||||||
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
|
|
||||||
if(repository.commitCount == 0){
|
|
||||||
repo.html.guide(repository)
|
|
||||||
} else {
|
|
||||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
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) =>
|
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
|
|
||||||
|
|
||||||
// get files
|
|
||||||
val files = JGitUtil.getFileList(git, revision, path)
|
|
||||||
// process README.md
|
|
||||||
val readme = files.find(_.name == "README.md").map { file =>
|
|
||||||
StringUtil.convertFromByteArray(JGitUtil.getContent(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)
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import util._
|
|
||||||
import service._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
class SearchController extends SearchControllerBase
|
|
||||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
|
||||||
with RepositorySearchService with IssuesService
|
|
||||||
with ReferrerAuthenticator
|
|
||||||
|
|
||||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
|
||||||
with SystemSettingsService 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 =>
|
|
||||||
val query = params("q").trim
|
|
||||||
val target = params.getOrElse("type", "code")
|
|
||||||
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,53 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
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 redirect = params.get("redirect")
|
|
||||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
|
||||||
session.setAttribute("REDIRECT", redirect.get)
|
|
||||||
}
|
|
||||||
html.signin(loadSystemSettings())
|
|
||||||
}
|
|
||||||
|
|
||||||
post("/signin", form){ form =>
|
|
||||||
val settings = loadSystemSettings()
|
|
||||||
authenticate(loadSystemSettings(), form.userName, form.password) match {
|
|
||||||
case Some(account) => signin(account)
|
|
||||||
case None => redirect("/signin")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get("/signout"){
|
|
||||||
session.invalidate
|
|
||||||
redirect("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set account information into HttpSession and redirect.
|
|
||||||
*/
|
|
||||||
private def signin(account: model.Account) = {
|
|
||||||
session.setAttribute("LOGIN_ACCOUNT", account)
|
|
||||||
updateLastLoginDate(account.userName)
|
|
||||||
|
|
||||||
session.get("REDIRECT").map { redirectUrl =>
|
|
||||||
session.removeAttribute("REDIRECT")
|
|
||||||
redirect(redirectUrl.asInstanceOf[String])
|
|
||||||
}.getOrElse {
|
|
||||||
redirect("/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service.{AccountService, SystemSettingsService}
|
|
||||||
import SystemSettingsService._
|
|
||||||
import util.AdminAuthenticator
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
import org.scalatra.FlashMapSupport
|
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
|
||||||
with SystemSettingsService with AccountService with AdminAuthenticator
|
|
||||||
|
|
||||||
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
|
||||||
self: SystemSettingsService with AccountService with AdminAuthenticator =>
|
|
||||||
|
|
||||||
private val form = mapping(
|
|
||||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
|
||||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
|
||||||
"notification" -> trim(label("Notification", boolean())),
|
|
||||||
"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())))
|
|
||||||
)(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))),
|
|
||||||
"mailAttribute" -> trim(label("Mail address attribute", text(required)))
|
|
||||||
)(Ldap.apply))
|
|
||||||
)(SystemSettings.apply)
|
|
||||||
|
|
||||||
|
|
||||||
get("/admin/system")(adminOnly {
|
|
||||||
admin.html.system(loadSystemSettings(), flash.get("info"))
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/system", form)(adminOnly { form =>
|
|
||||||
saveSystemSettings(form)
|
|
||||||
flash += "info" -> "System settings has been updated."
|
|
||||||
redirect("/admin/system")
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import util.AdminAuthenticator
|
|
||||||
import util.StringUtil._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
class UserManagementController extends UserManagementControllerBase
|
|
||||||
with AccountService with RepositoryService with AdminAuthenticator
|
|
||||||
|
|
||||||
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|
||||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
|
||||||
|
|
||||||
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
|
|
||||||
url: Option[String], fileId: Option[String])
|
|
||||||
|
|
||||||
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
|
|
||||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
|
||||||
|
|
||||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
|
||||||
memberNames: Option[String])
|
|
||||||
|
|
||||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
|
||||||
memberNames: Option[String], clearImage: Boolean)
|
|
||||||
|
|
||||||
val newUserForm = mapping(
|
|
||||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
|
|
||||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
|
||||||
"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))))),
|
|
||||||
"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()))
|
|
||||||
)(EditUserForm.apply)
|
|
||||||
|
|
||||||
val newGroupForm = 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()))),
|
|
||||||
"memberNames" -> trim(label("Member Names" , optional(text())))
|
|
||||||
)(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()))),
|
|
||||||
"memberNames" -> trim(label("Member Names" , optional(text()))),
|
|
||||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
|
||||||
)(EditGroupForm.apply)
|
|
||||||
|
|
||||||
get("/admin/users")(adminOnly {
|
|
||||||
val users = getAllUsers()
|
|
||||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
|
||||||
account.userName -> getGroupMembers(account.userName)
|
|
||||||
}.toMap
|
|
||||||
admin.users.html.list(users, members)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/_newuser")(adminOnly {
|
|
||||||
admin.users.html.user(None)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
|
||||||
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
|
|
||||||
updateImage(form.userName, form.fileId, false)
|
|
||||||
redirect("/admin/users")
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
|
||||||
val userName = params("userName")
|
|
||||||
admin.users.html.user(getAccountByUserName(userName))
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
|
||||||
val userName = params("userName")
|
|
||||||
getAccountByUserName(userName).map { account =>
|
|
||||||
updateAccount(getAccountByUserName(userName).get.copy(
|
|
||||||
password = form.password.map(sha1).getOrElse(account.password),
|
|
||||||
mailAddress = form.mailAddress,
|
|
||||||
isAdmin = form.isAdmin,
|
|
||||||
url = form.url))
|
|
||||||
|
|
||||||
updateImage(userName, form.fileId, form.clearImage)
|
|
||||||
redirect("/admin/users")
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/_newgroup")(adminOnly {
|
|
||||||
admin.users.html.group(None, Nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
|
||||||
createGroup(form.groupName, form.url)
|
|
||||||
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
|
|
||||||
updateImage(form.groupName, form.fileId, false)
|
|
||||||
redirect("/admin/users")
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
|
||||||
val groupName = params("groupName")
|
|
||||||
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
|
||||||
val groupName = params("groupName")
|
|
||||||
getAccountByUserName(groupName).map { account =>
|
|
||||||
updateGroup(groupName, form.url)
|
|
||||||
|
|
||||||
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
|
|
||||||
updateGroupMembers(form.groupName, memberNames)
|
|
||||||
|
|
||||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
|
||||||
removeCollaborators(form.groupName, repositoryName)
|
|
||||||
memberNames.foreach { userName =>
|
|
||||||
addCollaborator(form.groupName, repositoryName, userName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
|
||||||
redirect("/admin/users")
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/_usercheck")(adminOnly {
|
|
||||||
getAccountByUserName(params("userName")).isDefined
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import service._
|
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
|
|
||||||
import util.Directory._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
|
||||||
|
|
||||||
class WikiController extends WikiControllerBase
|
|
||||||
with WikiService with RepositoryService with AccountService with ActivityService
|
|
||||||
with CollaboratorsAuthenticator with ReferrerAuthenticator
|
|
||||||
|
|
||||||
trait WikiControllerBase extends ControllerBase {
|
|
||||||
self: WikiService with RepositoryService with ActivityService
|
|
||||||
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
|
||||||
|
|
||||||
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
|
|
||||||
|
|
||||||
val newForm = mapping(
|
|
||||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
|
|
||||||
"content" -> trim(label("Content" , text(required))),
|
|
||||||
"message" -> trim(label("Message" , optional(text()))),
|
|
||||||
"currentPageName" -> trim(label("Current page name" , text()))
|
|
||||||
)(WikiPageEditForm.apply)
|
|
||||||
|
|
||||||
val editForm = mapping(
|
|
||||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
|
|
||||||
"content" -> trim(label("Content" , text(required))),
|
|
||||||
"message" -> trim(label("Message" , optional(text()))),
|
|
||||||
"currentPageName" -> trim(label("Current page name" , text(required)))
|
|
||||||
)(WikiPageEditForm.apply)
|
|
||||||
|
|
||||||
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
|
||||||
getWikiPage(repository.owner, repository.name, "Home").map { page =>
|
|
||||||
wiki.html.page("Home", page, 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 =>
|
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
|
||||||
|
|
||||||
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
|
||||||
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
|
||||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
|
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
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 =>
|
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
|
||||||
val commitId = params("commitId").split("\\.\\.\\.")
|
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
|
|
||||||
val commitId = params("commitId").split("\\.\\.\\.")
|
|
||||||
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
|
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
|
||||||
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
|
||||||
val loginAccount = context.loginAccount.get
|
|
||||||
|
|
||||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
|
||||||
form.content, loginAccount, form.message.getOrElse("")).map { commitId =>
|
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
|
||||||
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 {
|
|
||||||
wiki.html.edit("", None, _)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
|
||||||
val loginAccount = context.loginAccount.get
|
|
||||||
|
|
||||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
|
||||||
form.content, context.loginAccount.get, form.message.getOrElse(""))
|
|
||||||
|
|
||||||
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 =>
|
|
||||||
val pageName = StringUtil.urlDecode(params("page"))
|
|
||||||
val account = context.loginAccount.get
|
|
||||||
|
|
||||||
deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}")
|
|
||||||
updateLastActivityDate(repository.owner, repository.name)
|
|
||||||
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
|
||||||
wiki.html.pages(getWikiPageList(repository.owner, repository.name), repository,
|
|
||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
|
||||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
|
||||||
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 =>
|
|
||||||
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
content
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
private def unique: Constraint = new Constraint(){
|
|
||||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
|
||||||
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def pagename: Constraint = new Constraint(){
|
|
||||||
override def validate(name: String, value: String): 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.{Account, CommitState, CommitStatus}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
||||||
|
*/
|
||||||
|
case class ApiCombinedCommitStatus(
|
||||||
|
state: String,
|
||||||
|
sha: String,
|
||||||
|
total_count: Int,
|
||||||
|
statuses: Iterable[ApiCommitStatus],
|
||||||
|
repository: ApiRepository){
|
||||||
|
// val commit_url = ApiPath(s"/api/v3/repos/${repository.full_name}/${sha}")
|
||||||
|
val url = ApiPath(s"/api/v3/repos/${repository.full_name}/commits/${sha}/status")
|
||||||
|
}
|
||||||
|
object ApiCombinedCommitStatus {
|
||||||
|
def apply(sha:String, statuses: Iterable[(CommitStatus, Account)], repository:ApiRepository): ApiCombinedCommitStatus = ApiCombinedCommitStatus(
|
||||||
|
state = CommitState.combine(statuses.map(_._1.state).toSet).name,
|
||||||
|
sha = sha,
|
||||||
|
total_count= statuses.size,
|
||||||
|
statuses = statuses.map{ case (s, a)=> ApiCommitStatus(s, ApiUser(a)) },
|
||||||
|
repository = repository)
|
||||||
|
}
|
||||||
26
src/main/scala/gitbucket/core/api/ApiComment.scala
Normal file
26
src/main/scala/gitbucket/core/api/ApiComment.scala
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.IssueComment
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/
|
||||||
|
*/
|
||||||
|
case class ApiComment(
|
||||||
|
id: Int,
|
||||||
|
user: ApiUser,
|
||||||
|
body: String,
|
||||||
|
created_at: Date,
|
||||||
|
updated_at: Date)
|
||||||
|
|
||||||
|
object ApiComment{
|
||||||
|
def apply(comment: IssueComment, user: ApiUser): ApiComment =
|
||||||
|
ApiComment(
|
||||||
|
id = comment.commentId,
|
||||||
|
user = user,
|
||||||
|
body = comment.content,
|
||||||
|
created_at = comment.registeredDate,
|
||||||
|
updated_at = comment.updatedDate)
|
||||||
|
}
|
||||||
48
src/main/scala/gitbucket/core/api/ApiCommit.scala
Normal file
48
src/main/scala/gitbucket/core/api/ApiCommit.scala
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.util.JGitUtil
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
|
import gitbucket.core.util.RepositoryName
|
||||||
|
|
||||||
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/commits/
|
||||||
|
*/
|
||||||
|
case class ApiCommit(
|
||||||
|
id: String,
|
||||||
|
message: String,
|
||||||
|
timestamp: Date,
|
||||||
|
added: List[String],
|
||||||
|
removed: List[String],
|
||||||
|
modified: List[String],
|
||||||
|
author: ApiPersonIdent,
|
||||||
|
committer: ApiPersonIdent)(repositoryName:RepositoryName){
|
||||||
|
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
|
||||||
|
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
object ApiCommit{
|
||||||
|
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = {
|
||||||
|
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||||
|
ApiCommit(
|
||||||
|
id = commit.id,
|
||||||
|
message = commit.fullMessage,
|
||||||
|
timestamp = commit.commitTime,
|
||||||
|
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 = ApiPersonIdent.author(commit),
|
||||||
|
committer = ApiPersonIdent.committer(commit)
|
||||||
|
)(repositoryName)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/scala/gitbucket/core/api/ApiCommitListItem.scala
Normal file
42
src/main/scala/gitbucket/core/api/ApiCommitListItem.scala
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.api.ApiCommitListItem._
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
|
import gitbucket.core.util.RepositoryName
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/commits/
|
||||||
|
*/
|
||||||
|
case class ApiCommitListItem(
|
||||||
|
sha: String,
|
||||||
|
commit: Commit,
|
||||||
|
author: Option[ApiUser],
|
||||||
|
committer: Option[ApiUser],
|
||||||
|
parents: Seq[Parent])(repositoryName: RepositoryName) {
|
||||||
|
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
|
||||||
|
}
|
||||||
|
|
||||||
|
object ApiCommitListItem {
|
||||||
|
def apply(commit: CommitInfo, repositoryName: RepositoryName): ApiCommitListItem = ApiCommitListItem(
|
||||||
|
sha = commit.id,
|
||||||
|
commit = Commit(
|
||||||
|
message = commit.fullMessage,
|
||||||
|
author = ApiPersonIdent.author(commit),
|
||||||
|
committer = ApiPersonIdent.committer(commit)
|
||||||
|
)(commit.id, repositoryName),
|
||||||
|
author = None,
|
||||||
|
committer = None,
|
||||||
|
parents = commit.parents.map(Parent(_)(repositoryName)))(repositoryName)
|
||||||
|
|
||||||
|
case class Parent(sha: String)(repositoryName: RepositoryName){
|
||||||
|
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Commit(
|
||||||
|
message: String,
|
||||||
|
author: ApiPersonIdent,
|
||||||
|
committer: ApiPersonIdent)(sha:String, repositoryName: RepositoryName) {
|
||||||
|
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${sha}")
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/scala/gitbucket/core/api/ApiCommitStatus.scala
Normal file
38
src/main/scala/gitbucket/core/api/ApiCommitStatus.scala
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.CommitStatus
|
||||||
|
import gitbucket.core.util.RepositoryName
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||||
|
*/
|
||||||
|
case class ApiCommitStatus(
|
||||||
|
created_at: Date,
|
||||||
|
updated_at: Date,
|
||||||
|
state: String,
|
||||||
|
target_url: Option[String],
|
||||||
|
description: Option[String],
|
||||||
|
id: Int,
|
||||||
|
context: String,
|
||||||
|
creator: ApiUser
|
||||||
|
)(sha: String, repositoryName: RepositoryName) {
|
||||||
|
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}/statuses")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object ApiCommitStatus {
|
||||||
|
def apply(status: CommitStatus, creator:ApiUser): ApiCommitStatus = ApiCommitStatus(
|
||||||
|
created_at = status.registeredDate,
|
||||||
|
updated_at = status.updatedDate,
|
||||||
|
state = status.state.name,
|
||||||
|
target_url = status.targetUrl,
|
||||||
|
description= status.description,
|
||||||
|
id = status.commitStatusId,
|
||||||
|
context = status.context,
|
||||||
|
creator = creator
|
||||||
|
)(status.commitId, RepositoryName(status))
|
||||||
|
}
|
||||||
5
src/main/scala/gitbucket/core/api/ApiError.scala
Normal file
5
src/main/scala/gitbucket/core/api/ApiError.scala
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
case class ApiError(
|
||||||
|
message: String,
|
||||||
|
documentation_url: Option[String] = None)
|
||||||
31
src/main/scala/gitbucket/core/api/ApiIssue.scala
Normal file
31
src/main/scala/gitbucket/core/api/ApiIssue.scala
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.Issue
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/
|
||||||
|
*/
|
||||||
|
case class ApiIssue(
|
||||||
|
number: Int,
|
||||||
|
title: String,
|
||||||
|
user: ApiUser,
|
||||||
|
// labels,
|
||||||
|
state: String,
|
||||||
|
created_at: Date,
|
||||||
|
updated_at: Date,
|
||||||
|
body: String)
|
||||||
|
|
||||||
|
object ApiIssue{
|
||||||
|
def apply(issue: Issue, user: ApiUser): ApiIssue =
|
||||||
|
ApiIssue(
|
||||||
|
number = issue.issueId,
|
||||||
|
title = issue.title,
|
||||||
|
user = user,
|
||||||
|
state = if(issue.closed){ "closed" }else{ "open" },
|
||||||
|
body = issue.content.getOrElse(""),
|
||||||
|
created_at = issue.registeredDate,
|
||||||
|
updated_at = issue.updatedDate)
|
||||||
|
}
|
||||||
6
src/main/scala/gitbucket/core/api/ApiPath.scala
Normal file
6
src/main/scala/gitbucket/core/api/ApiPath.scala
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
|
||||||
|
*/
|
||||||
|
case class ApiPath(path: String)
|
||||||
25
src/main/scala/gitbucket/core/api/ApiPersonIdent.scala
Normal file
25
src/main/scala/gitbucket/core/api/ApiPersonIdent.scala
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
|
case class ApiPersonIdent(
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
date: Date)
|
||||||
|
|
||||||
|
|
||||||
|
object ApiPersonIdent {
|
||||||
|
def author(commit: CommitInfo): ApiPersonIdent =
|
||||||
|
ApiPersonIdent(
|
||||||
|
name = commit.authorName,
|
||||||
|
email = commit.authorEmailAddress,
|
||||||
|
date = commit.authorTime)
|
||||||
|
def committer(commit: CommitInfo): ApiPersonIdent =
|
||||||
|
ApiPersonIdent(
|
||||||
|
name = commit.committerName,
|
||||||
|
email = commit.committerEmailAddress,
|
||||||
|
date = commit.commitTime)
|
||||||
|
}
|
||||||
59
src/main/scala/gitbucket/core/api/ApiPullRequest.scala
Normal file
59
src/main/scala/gitbucket/core/api/ApiPullRequest.scala
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.{Issue, PullRequest}
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/
|
||||||
|
*/
|
||||||
|
case class ApiPullRequest(
|
||||||
|
number: Int,
|
||||||
|
updated_at: Date,
|
||||||
|
created_at: Date,
|
||||||
|
head: ApiPullRequest.Commit,
|
||||||
|
base: ApiPullRequest.Commit,
|
||||||
|
mergeable: Option[Boolean],
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
user: ApiUser) {
|
||||||
|
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
|
||||||
|
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
|
||||||
|
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
|
||||||
|
val url = ApiPath(s"${base.repo.url.path}/pulls/${number}")
|
||||||
|
//val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}")
|
||||||
|
val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits")
|
||||||
|
val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments")
|
||||||
|
val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}")
|
||||||
|
val comments_url = ApiPath(s"${base.repo.url.path}/issues/${number}/comments")
|
||||||
|
val statuses_url = ApiPath(s"${base.repo.url.path}/statuses/${head.sha}")
|
||||||
|
}
|
||||||
|
|
||||||
|
object ApiPullRequest{
|
||||||
|
def apply(issue: Issue, pullRequest: PullRequest, headRepo: ApiRepository, baseRepo: ApiRepository, user: ApiUser): ApiPullRequest = ApiPullRequest(
|
||||||
|
number = issue.issueId,
|
||||||
|
updated_at = issue.updatedDate,
|
||||||
|
created_at = issue.registeredDate,
|
||||||
|
head = Commit(
|
||||||
|
sha = pullRequest.commitIdTo,
|
||||||
|
ref = pullRequest.requestBranch,
|
||||||
|
repo = headRepo)(issue.userName),
|
||||||
|
base = Commit(
|
||||||
|
sha = pullRequest.commitIdFrom,
|
||||||
|
ref = pullRequest.branch,
|
||||||
|
repo = baseRepo)(issue.userName),
|
||||||
|
mergeable = None, // TODO: need check mergeable.
|
||||||
|
title = issue.title,
|
||||||
|
body = issue.content.getOrElse(""),
|
||||||
|
user = user
|
||||||
|
)
|
||||||
|
|
||||||
|
case class Commit(
|
||||||
|
sha: String,
|
||||||
|
ref: String,
|
||||||
|
repo: ApiRepository)(baseOwner:String){
|
||||||
|
val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
|
||||||
|
val user = repo.owner
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/main/scala/gitbucket/core/api/ApiRepository.scala
Normal file
48
src/main/scala/gitbucket/core/api/ApiRepository.scala
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.{Account, Repository}
|
||||||
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
|
|
||||||
|
|
||||||
|
// https://developer.github.com/v3/repos/
|
||||||
|
case class ApiRepository(
|
||||||
|
name: String,
|
||||||
|
full_name: String,
|
||||||
|
description: String,
|
||||||
|
watchers: Int,
|
||||||
|
forks: Int,
|
||||||
|
`private`: Boolean,
|
||||||
|
default_branch: String,
|
||||||
|
owner: ApiUser) {
|
||||||
|
val forks_count = forks
|
||||||
|
val watchers_coun = watchers
|
||||||
|
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
||||||
|
val http_url = ApiPath(s"/git/${full_name}.git")
|
||||||
|
val clone_url = ApiPath(s"/git/${full_name}.git")
|
||||||
|
val html_url = ApiPath(s"/${full_name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
object ApiRepository{
|
||||||
|
def apply(
|
||||||
|
repository: Repository,
|
||||||
|
owner: ApiUser,
|
||||||
|
forkedCount: Int =0,
|
||||||
|
watchers: Int = 0): ApiRepository =
|
||||||
|
ApiRepository(
|
||||||
|
name = repository.repositoryName,
|
||||||
|
full_name = s"${repository.userName}/${repository.repositoryName}",
|
||||||
|
description = repository.description.getOrElse(""),
|
||||||
|
watchers = 0,
|
||||||
|
forks = forkedCount,
|
||||||
|
`private` = repository.isPrivate,
|
||||||
|
default_branch = repository.defaultBranch,
|
||||||
|
owner = owner
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||||
|
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
|
||||||
|
|
||||||
|
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
||||||
|
this(repositoryInfo.repository, ApiUser(owner))
|
||||||
|
|
||||||
|
}
|
||||||
36
src/main/scala/gitbucket/core/api/ApiUser.scala
Normal file
36
src/main/scala/gitbucket/core/api/ApiUser.scala
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.Account
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
|
||||||
|
case class ApiUser(
|
||||||
|
login: String,
|
||||||
|
email: String,
|
||||||
|
`type`: String,
|
||||||
|
site_admin: Boolean,
|
||||||
|
created_at: Date) {
|
||||||
|
val url = ApiPath(s"/api/v3/users/${login}")
|
||||||
|
val html_url = ApiPath(s"/${login}")
|
||||||
|
// val followers_url = ApiPath(s"/api/v3/users/${login}/followers")
|
||||||
|
// val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}")
|
||||||
|
// val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}")
|
||||||
|
// val starred_url = ApiPath(s"/api/v3/users/${login}/starred{/owner}{/repo}")
|
||||||
|
// val subscriptions_url = ApiPath(s"/api/v3/users/${login}/subscriptions")
|
||||||
|
// val organizations_url = ApiPath(s"/api/v3/users/${login}/orgs")
|
||||||
|
// val repos_url = ApiPath(s"/api/v3/users/${login}/repos")
|
||||||
|
// val events_url = ApiPath(s"/api/v3/users/${login}/events{/privacy}")
|
||||||
|
// val received_events_url = ApiPath(s"/api/v3/users/${login}/received_events")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object ApiUser{
|
||||||
|
def apply(user: Account): ApiUser = ApiUser(
|
||||||
|
login = user.fullName,
|
||||||
|
email = user.mailAddress,
|
||||||
|
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" },
|
||||||
|
site_admin = user.isAdmin,
|
||||||
|
created_at = user.registeredDate
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/main/scala/gitbucket/core/api/CreateAComment.scala
Normal file
7
src/main/scala/gitbucket/core/api/CreateAComment.scala
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||||
|
* api form
|
||||||
|
*/
|
||||||
|
case class CreateAComment(body: String)
|
||||||
26
src/main/scala/gitbucket/core/api/CreateAStatus.scala
Normal file
26
src/main/scala/gitbucket/core/api/CreateAStatus.scala
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import gitbucket.core.model.CommitState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||||
|
* api form
|
||||||
|
*/
|
||||||
|
case class CreateAStatus(
|
||||||
|
/* state is Required. The state of the status. Can be one of pending, success, error, or failure. */
|
||||||
|
state: String,
|
||||||
|
/* context is a string label to differentiate this status from the status of other systems. Default: "default" */
|
||||||
|
context: Option[String],
|
||||||
|
/* The target URL to associate with this status. This URL will be linked from the GitHub UI to allow users to easily see the ‘source’ of the Status. */
|
||||||
|
target_url: Option[String],
|
||||||
|
/* description is a short description of the status.*/
|
||||||
|
description: Option[String]
|
||||||
|
) {
|
||||||
|
def isValid: Boolean = {
|
||||||
|
CommitState.valueOf(state).isDefined &&
|
||||||
|
// only http
|
||||||
|
target_url.filterNot(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length<255).isEmpty &&
|
||||||
|
context.filterNot(f => f.length<255).isEmpty &&
|
||||||
|
description.filterNot(f => f.length<1000).isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/main/scala/gitbucket/core/api/JsonFormat.scala
Normal file
44
src/main/scala/gitbucket/core/api/JsonFormat.scala
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
import org.joda.time.DateTimeZone
|
||||||
|
import org.joda.time.format._
|
||||||
|
import org.json4s._
|
||||||
|
import org.json4s.jackson.Serialization
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
|
||||||
|
object JsonFormat {
|
||||||
|
case class Context(baseUrl:String)
|
||||||
|
val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||||
|
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
|
||||||
|
(
|
||||||
|
{ case JString(s) => Try(parserISO.parseDateTime(s)).toOption.map(_.toDate)
|
||||||
|
.getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
|
||||||
|
{ case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) }
|
||||||
|
)
|
||||||
|
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
|
||||||
|
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
|
||||||
|
FieldSerializer[ApiCommitStatus]() + FieldSerializer[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() +
|
||||||
|
FieldSerializer[ApiPullRequest.Commit]()
|
||||||
|
|
||||||
|
|
||||||
|
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
|
||||||
|
(
|
||||||
|
{
|
||||||
|
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
|
||||||
|
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case ApiPath(path) => JString(c.baseUrl+path)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* convert object to json string
|
||||||
|
*/
|
||||||
|
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
|
||||||
|
}
|
||||||
535
src/main/scala/gitbucket/core/controller/AccountController.scala
Normal file
535
src/main/scala/gitbucket/core/controller/AccountController.scala
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.account.html
|
||||||
|
import gitbucket.core.api._
|
||||||
|
import gitbucket.core.helper
|
||||||
|
import gitbucket.core.model.GroupMember
|
||||||
|
import gitbucket.core.service._
|
||||||
|
import gitbucket.core.ssh.SshUtil
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.StringUtil._
|
||||||
|
import gitbucket.core.util._
|
||||||
|
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
|
|
||||||
|
class AccountController extends AccountControllerBase
|
||||||
|
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||||
|
with AccessTokenService with WebHookService
|
||||||
|
|
||||||
|
|
||||||
|
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||||
|
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||||
|
with AccessTokenService with WebHookService =>
|
||||||
|
|
||||||
|
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||||
|
url: Option[String], fileId: 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)
|
||||||
|
|
||||||
|
case class PersonalTokenForm(note: String)
|
||||||
|
|
||||||
|
val newForm = mapping(
|
||||||
|
"userName" -> trim(label("User name" , 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()))),
|
||||||
|
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" , optional(text())))
|
||||||
|
)(AccountNewForm.apply)
|
||||||
|
|
||||||
|
val editForm = mapping(
|
||||||
|
"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")))),
|
||||||
|
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||||
|
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||||
|
)(AccountEditForm.apply)
|
||||||
|
|
||||||
|
val sshKeyForm = mapping(
|
||||||
|
"title" -> trim(label("Title", text(required, maxlength(100)))),
|
||||||
|
"publicKey" -> trim(label("Key" , text(required, validPublicKey)))
|
||||||
|
)(SshKeyForm.apply)
|
||||||
|
|
||||||
|
val personalTokenForm = mapping(
|
||||||
|
"note" -> trim(label("Token", text(required, maxlength(100))))
|
||||||
|
)(PersonalTokenForm.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)
|
||||||
|
|
||||||
|
case class AccountForm(accountName: String)
|
||||||
|
|
||||||
|
val accountForm = mapping(
|
||||||
|
"account" -> trim(label("Group/User name", text(required, validAccountName)))
|
||||||
|
)(AccountForm.apply)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays user information.
|
||||||
|
*/
|
||||||
|
get("/:userName") {
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).map { account =>
|
||||||
|
params.getOrElse("tab", "repositories") match {
|
||||||
|
// Public Activity
|
||||||
|
case "activity" =>
|
||||||
|
gitbucket.core.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)
|
||||||
|
gitbucket.core.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)
|
||||||
|
gitbucket.core.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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))
|
||||||
|
} getOrElse {
|
||||||
|
contentType = "image/png"
|
||||||
|
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/users/#get-a-single-user
|
||||||
|
*/
|
||||||
|
get("/api/v3/users/:userName") {
|
||||||
|
getAccountByUserName(params("userName")).map { account =>
|
||||||
|
JsonFormat(ApiUser(account))
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||||
|
*/
|
||||||
|
get("/api/v3/user") {
|
||||||
|
context.loginAccount.map { account =>
|
||||||
|
JsonFormat(ApiUser(account))
|
||||||
|
} getOrElse Unauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get("/:userName/_edit")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).map { x =>
|
||||||
|
html.edit(x, flash.get("info"))
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:userName/_edit", editForm)(oneselfOnly { form =>
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).map { account =>
|
||||||
|
updateAccount(account.copy(
|
||||||
|
password = form.password.map(sha1).getOrElse(account.password),
|
||||||
|
fullName = form.fullName,
|
||||||
|
mailAddress = form.mailAddress,
|
||||||
|
url = form.url))
|
||||||
|
|
||||||
|
updateImage(userName, form.fileId, form.clearImage)
|
||||||
|
flash += "info" -> "Account information has been updated."
|
||||||
|
redirect(s"/${userName}/_edit")
|
||||||
|
|
||||||
|
} 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 =>
|
||||||
|
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("/:userName/_application")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).map { x =>
|
||||||
|
var tokens = getAccessTokens(x.userName)
|
||||||
|
val generatedToken = flash.get("generatedToken") match {
|
||||||
|
case Some((tokenId:Int, token:String)) => {
|
||||||
|
val gt = tokens.find(_.accessTokenId == tokenId)
|
||||||
|
gt.map{ t =>
|
||||||
|
tokens = tokens.filterNot(_ == t)
|
||||||
|
(t, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
html.application(x, tokens, generatedToken)
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName).map { x =>
|
||||||
|
val (tokenId, token) = generateAccessToken(userName, form.note)
|
||||||
|
flash += "generatedToken" -> (tokenId, token)
|
||||||
|
}
|
||||||
|
redirect(s"/${userName}/_application")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:userName/_personalToken/delete/:id")(oneselfOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
val tokenId = params("id").toInt
|
||||||
|
deleteAccessToken(userName, tokenId)
|
||||||
|
redirect(s"/${userName}/_application")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/register"){
|
||||||
|
if(context.settings.allowAccountRegistration){
|
||||||
|
if(context.loginAccount.isDefined){
|
||||||
|
redirect("/")
|
||||||
|
} else {
|
||||||
|
html.register()
|
||||||
|
}
|
||||||
|
} else NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/register", newForm){ form =>
|
||||||
|
if(context.settings.allowAccountRegistration){
|
||||||
|
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
|
||||||
|
updateImage(form.userName, form.fileId, false)
|
||||||
|
redirect("/signin")
|
||||||
|
} else NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/groups/new")(usersOnly {
|
||||||
|
html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
|
||||||
|
})
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
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 {
|
||||||
|
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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),
|
||||||
|
Constants.HEAD, 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
|
||||||
|
val groups = getGroupsByUserName(loginUserName)
|
||||||
|
groups match {
|
||||||
|
case _: List[String] =>
|
||||||
|
val managerPermissions = groups.map { group =>
|
||||||
|
val members = getGroupMembers(group)
|
||||||
|
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
|
||||||
|
}
|
||||||
|
helper.html.forkrepository(
|
||||||
|
repository,
|
||||||
|
(groups zip managerPermissions).toMap
|
||||||
|
)
|
||||||
|
case _ => redirect(s"/${loginUserName}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
val loginAccount = context.loginAccount.get
|
||||||
|
val loginUserName = loginAccount.userName
|
||||||
|
val accountName = form.accountName
|
||||||
|
|
||||||
|
LockUtil.lock(s"${accountName}/${repository.name}"){
|
||||||
|
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
|
||||||
|
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
|
||||||
|
// redirect to the repository if repository already exists
|
||||||
|
redirect(s"/${accountName}/${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 = accountName,
|
||||||
|
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(accountName, repository.name)
|
||||||
|
|
||||||
|
// clone repository actually
|
||||||
|
JGitUtil.cloneRepository(
|
||||||
|
getRepositoryDir(repository.owner, repository.name),
|
||||||
|
getRepositoryDir(accountName, repository.name))
|
||||||
|
|
||||||
|
// Create Wiki repository
|
||||||
|
JGitUtil.cloneRepository(
|
||||||
|
getWikiRepositoryDir(repository.owner, repository.name),
|
||||||
|
getWikiRepositoryDir(accountName, repository.name))
|
||||||
|
|
||||||
|
// Record activity
|
||||||
|
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
|
||||||
|
// redirect to the repository
|
||||||
|
redirect(s"/${accountName}/${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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def validAccountName: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
getAccountByUserName(value) match {
|
||||||
|
case Some(_) => None
|
||||||
|
case None => Some("Invalid Group/User Account.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
class AnonymousAccessController extends AnonymousAccessControllerBase
|
||||||
|
|
||||||
|
trait AnonymousAccessControllerBase extends ControllerBase {
|
||||||
|
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
|
||||||
|
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
|
||||||
|
!context.currentPath.startsWith("/register")) {
|
||||||
|
Unauthorized()
|
||||||
|
} else {
|
||||||
|
pass()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/main/scala/gitbucket/core/controller/ControllerBase.scala
Normal file
237
src/main/scala/gitbucket/core/controller/ControllerBase.scala
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.api.ApiError
|
||||||
|
import gitbucket.core.model.Account
|
||||||
|
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util._
|
||||||
|
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.json4s._
|
||||||
|
import org.scalatra._
|
||||||
|
import org.scalatra.i18n._
|
||||||
|
import org.scalatra.json._
|
||||||
|
|
||||||
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
|
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||||
|
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides generic features for controller implementations.
|
||||||
|
*/
|
||||||
|
abstract class ControllerBase extends ScalatraFilter
|
||||||
|
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
|
||||||
|
with SystemSettingsService {
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if(path.startsWith("/api/v3/")){
|
||||||
|
httpRequest.setAttribute(Keys.Request.APIv3, true)
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
*/
|
||||||
|
implicit def context: Context = {
|
||||||
|
contextCache.get match {
|
||||||
|
case null => {
|
||||||
|
val context = Context(loadSystemSettings(), LoginAccount, request)
|
||||||
|
contextCache.set(context)
|
||||||
|
context
|
||||||
|
}
|
||||||
|
case context => context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def LoginAccount: Option[Account] = request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
|
||||||
|
|
||||||
|
def ajaxGet(path : String)(action : => Any) : Route =
|
||||||
|
super.get(path){
|
||||||
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||||
|
super.ajaxGet(path, form){ form =>
|
||||||
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
|
action(form)
|
||||||
|
}
|
||||||
|
|
||||||
|
def ajaxPost(path : String)(action : => Any) : Route =
|
||||||
|
super.post(path){
|
||||||
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route =
|
||||||
|
super.ajaxPost(path, form){ form =>
|
||||||
|
request.setAttribute(Keys.Request.Ajax, "true")
|
||||||
|
action(form)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def NotFound() =
|
||||||
|
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||||
|
org.scalatra.NotFound()
|
||||||
|
} else if(request.hasAttribute(Keys.Request.APIv3)){
|
||||||
|
contentType = formats("json")
|
||||||
|
org.scalatra.NotFound(ApiError("Not Found"))
|
||||||
|
} else {
|
||||||
|
org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def Unauthorized()(implicit context: Context) =
|
||||||
|
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||||
|
org.scalatra.Unauthorized()
|
||||||
|
} else if(request.hasAttribute(Keys.Request.APIv3)){
|
||||||
|
contentType = formats("json")
|
||||||
|
org.scalatra.Unauthorized(ApiError("Requires authentication"))
|
||||||
|
} else {
|
||||||
|
if(context.loginAccount.isDefined){
|
||||||
|
org.scalatra.Unauthorized(redirect("/"))
|
||||||
|
} else {
|
||||||
|
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 "")
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Scala 2.11
|
||||||
|
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
|
||||||
|
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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to response the raw data against XSS.
|
||||||
|
*/
|
||||||
|
protected def RawData[T](contentType: String, rawData: T): T = {
|
||||||
|
if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){
|
||||||
|
this.contentType = "text/plain"
|
||||||
|
} else {
|
||||||
|
this.contentType = contentType
|
||||||
|
}
|
||||||
|
response.addHeader("X-Content-Type-Options", "nosniff")
|
||||||
|
rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
// jenkins send message as 'application/x-www-form-urlencoded' but scalatra already parsed as multi-part-request.
|
||||||
|
def extractFromJsonBody[A](implicit request:HttpServletRequest, mf:Manifest[A]): Option[A] = {
|
||||||
|
(request.contentType.map(_.split(";").head.toLowerCase) match{
|
||||||
|
case Some("application/x-www-form-urlencoded") => multiParams.keys.headOption.map(parse(_))
|
||||||
|
case Some("application/json") => Some(parsedBody)
|
||||||
|
case _ => Some(parse(request.body))
|
||||||
|
}).filterNot(_ == JNothing).flatMap(j => Try(j.extract[A]).toOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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." }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.dashboard.html
|
||||||
|
import gitbucket.core.service.{RepositoryService, PullRequestService, AccountService, IssuesService}
|
||||||
|
import gitbucket.core.util.{StringUtil, Keys, UsersAuthenticator}
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.service.IssuesService._
|
||||||
|
|
||||||
|
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 AccountService
|
||||||
|
with UsersAuthenticator =>
|
||||||
|
|
||||||
|
get("/dashboard/issues")(usersOnly {
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
val account = context.loginAccount.get
|
||||||
|
Option(q).map { q =>
|
||||||
|
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
q match {
|
||||||
|
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
|
||||||
|
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
|
||||||
|
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
|
||||||
|
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
|
||||||
|
case _ => searchIssues("created_by")
|
||||||
|
}
|
||||||
|
} getOrElse {
|
||||||
|
searchIssues("created_by")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/issues/assigned")(usersOnly {
|
||||||
|
searchIssues("assigned")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/issues/created_by")(usersOnly {
|
||||||
|
searchIssues("created_by")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/issues/mentioned")(usersOnly {
|
||||||
|
searchIssues("mentioned")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls")(usersOnly {
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
val account = context.loginAccount.get
|
||||||
|
Option(q).map { q =>
|
||||||
|
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
q match {
|
||||||
|
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
|
||||||
|
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
|
||||||
|
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
|
||||||
|
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
|
||||||
|
case _ => searchPullRequests("created_by")
|
||||||
|
}
|
||||||
|
} getOrElse {
|
||||||
|
searchPullRequests("created_by")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls/created_by")(usersOnly {
|
||||||
|
searchPullRequests("created_by")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls/assigned")(usersOnly {
|
||||||
|
searchPullRequests("assigned")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls/mentioned")(usersOnly {
|
||||||
|
searchPullRequests("mentioned")
|
||||||
|
})
|
||||||
|
|
||||||
|
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
|
||||||
|
val condition = session.putAndGet(key, if(request.hasQueryString){
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
if(q == null){
|
||||||
|
IssueSearchCondition(request)
|
||||||
|
} else {
|
||||||
|
IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
}
|
||||||
|
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
|
||||||
|
|
||||||
|
filter match {
|
||||||
|
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
|
||||||
|
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
|
||||||
|
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def searchIssues(filter: String) = {
|
||||||
|
import IssuesService._
|
||||||
|
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
|
||||||
|
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|
||||||
|
html.issues(
|
||||||
|
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
||||||
|
page,
|
||||||
|
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
|
||||||
|
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
|
||||||
|
filter match {
|
||||||
|
case "assigned" => condition.copy(assigned = Some(userName))
|
||||||
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
|
case _ => condition.copy(author = Some(userName))
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
getGroupNames(userName))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def searchPullRequests(filter: String) = {
|
||||||
|
import IssuesService._
|
||||||
|
import PullRequestService._
|
||||||
|
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
|
||||||
|
val allRepos = getAllRepositories(userName)
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|
||||||
|
html.pulls(
|
||||||
|
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
||||||
|
page,
|
||||||
|
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
|
||||||
|
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
|
||||||
|
filter match {
|
||||||
|
case "assigned" => condition.copy(assigned = Some(userName))
|
||||||
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
|
case _ => condition.copy(author = Some(userName))
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
getGroupNames(userName))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.util.{Keys, FileUtil}
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
122
src/main/scala/gitbucket/core/controller/IndexController.scala
Normal file
122
src/main/scala/gitbucket/core/controller/IndexController.scala
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.api._
|
||||||
|
import gitbucket.core.helper.xml
|
||||||
|
import gitbucket.core.html
|
||||||
|
import gitbucket.core.model.Account
|
||||||
|
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService}
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
|
||||||
|
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
|
|
||||||
|
class IndexController extends IndexControllerBase
|
||||||
|
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
xml.feed(getRecentActivities())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set account information into HttpSession and redirect.
|
||||||
|
*/
|
||||||
|
private def signin(account: 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
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
|
||||||
|
* but not enabled.
|
||||||
|
*/
|
||||||
|
get("/api/v3/rate_limit"){
|
||||||
|
contentType = formats("json")
|
||||||
|
// this message is same as github enterprise...
|
||||||
|
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
||||||
|
}
|
||||||
|
}
|
||||||
469
src/main/scala/gitbucket/core/controller/IssuesController.scala
Normal file
469
src/main/scala/gitbucket/core/controller/IssuesController.scala
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.api._
|
||||||
|
import gitbucket.core.issues.html
|
||||||
|
import gitbucket.core.model.Issue
|
||||||
|
import gitbucket.core.service.IssuesService._
|
||||||
|
import gitbucket.core.service._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util._
|
||||||
|
import gitbucket.core.view
|
||||||
|
import gitbucket.core.view.Markdown
|
||||||
|
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.scalatra.Ok
|
||||||
|
|
||||||
|
|
||||||
|
class IssuesController extends IssuesControllerBase
|
||||||
|
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||||
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
|
||||||
|
|
||||||
|
trait IssuesControllerBase extends ControllerBase {
|
||||||
|
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||||
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
|
||||||
|
|
||||||
|
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 IssueStateForm(issueId: Int, content: Option[String])
|
||||||
|
|
||||||
|
val issueCreateForm = mapping(
|
||||||
|
"title" -> trim(label("Title", text(required))),
|
||||||
|
"content" -> trim(optional(text())),
|
||||||
|
"assignedUserName" -> trim(optional(text())),
|
||||||
|
"milestoneId" -> trim(optional(number())),
|
||||||
|
"labelNames" -> trim(optional(text()))
|
||||||
|
)(IssueCreateForm.apply)
|
||||||
|
|
||||||
|
val issueTitleEditForm = mapping(
|
||||||
|
"title" -> trim(label("Title", text(required)))
|
||||||
|
)(x => x)
|
||||||
|
val issueEditForm = mapping(
|
||||||
|
"content" -> trim(optional(text()))
|
||||||
|
)(x => x)
|
||||||
|
|
||||||
|
val commentForm = mapping(
|
||||||
|
"issueId" -> label("Issue Id", number()),
|
||||||
|
"content" -> trim(label("Comment", text(required)))
|
||||||
|
)(CommentForm.apply)
|
||||||
|
|
||||||
|
val issueStateForm = mapping(
|
||||||
|
"issueId" -> label("Issue Id", number()),
|
||||||
|
"content" -> trim(optional(text()))
|
||||||
|
)(IssueStateForm.apply)
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues")(referrersOnly { repository =>
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
if(Option(q).exists(_.contains("is:pr"))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
|
||||||
|
} else {
|
||||||
|
searchIssues(repository)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
||||||
|
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
|
||||||
|
getIssue(owner, name, issueId) map {
|
||||||
|
html.issue(
|
||||||
|
_,
|
||||||
|
getComments(owner, name, issueId.toInt),
|
||||||
|
getIssueLabels(owner, name, issueId.toInt),
|
||||||
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
|
getMilestonesWithIssueCount(owner, name),
|
||||||
|
getLabels(owner, name),
|
||||||
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
|
repository)
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, ApiUser(user)) })
|
||||||
|
}).getOrElse(NotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
html.create(
|
||||||
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
|
getMilestones(owner, name),
|
||||||
|
getLabels(owner, name),
|
||||||
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
|
repository)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
val writable = hasWritePermission(owner, name, context.loginAccount)
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
|
||||||
|
// insert issue
|
||||||
|
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
||||||
|
if(writable) form.assignedUserName else None,
|
||||||
|
if(writable) form.milestoneId else None)
|
||||||
|
|
||||||
|
// insert labels
|
||||||
|
if(writable){
|
||||||
|
form.labelNames.map { value =>
|
||||||
|
val labels = getLabels(owner, name)
|
||||||
|
value.split(",").foreach { labelName =>
|
||||||
|
labels.find(_.labelName == labelName).map { label =>
|
||||||
|
registerIssueLabel(owner, name, issueId, label.labelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
||||||
|
|
||||||
|
getIssue(owner, name, issueId.toString).foreach { issue =>
|
||||||
|
// extract references and create refer comment
|
||||||
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||||
|
|
||||||
|
// call web hooks
|
||||||
|
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||||
|
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getIssue(owner, name, params("id")).map { issue =>
|
||||||
|
if(isEditable(owner, name, issue.openedUserName)){
|
||||||
|
// update issue
|
||||||
|
updateIssue(owner, name, issue.issueId, title, issue.content)
|
||||||
|
// extract references and create refer comment
|
||||||
|
createReferComment(owner, name, issue.copy(title = title), title)
|
||||||
|
|
||||||
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getIssue(owner, name, params("id")).map { issue =>
|
||||||
|
if(isEditable(owner, name, issue.openedUserName)){
|
||||||
|
// update issue
|
||||||
|
updateIssue(owner, name, issue.issueId, issue.title, content)
|
||||||
|
// extract references and create refer comment
|
||||||
|
createReferComment(owner, name, issue, content.getOrElse(""))
|
||||||
|
|
||||||
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
|
||||||
|
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
||||||
|
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiComment(issueComment, ApiUser(context.loginAccount.get)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
updateComment(comment.commentId, form.content)
|
||||||
|
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
Ok(deleteComment(comment.commentId))
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
|
||||||
|
getIssue(repository.owner, repository.name, params("id")) map { x =>
|
||||||
|
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
|
||||||
|
params.get("dataType") collect {
|
||||||
|
case t if t == "html" => html.editissue(
|
||||||
|
x.content, x.issueId, x.userName, x.repositoryName)
|
||||||
|
} getOrElse {
|
||||||
|
contentType = formats("json")
|
||||||
|
org.json4s.jackson.Serialization.write(
|
||||||
|
Map("title" -> x.title,
|
||||||
|
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
|
||||||
|
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
|
||||||
|
getComment(repository.owner, repository.name, params("id")) map { x =>
|
||||||
|
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
||||||
|
params.get("dataType") collect {
|
||||||
|
case t if t == "html" => html.editcomment(
|
||||||
|
x.content, x.commentId, x.userName, x.repositoryName)
|
||||||
|
} getOrElse {
|
||||||
|
contentType = formats("json")
|
||||||
|
org.json4s.jackson.Serialization.write(
|
||||||
|
Map("content" -> view.Markdown.toHtml(x.content,
|
||||||
|
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
||||||
|
defining(params("id").toInt){ issueId =>
|
||||||
|
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||||
|
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
||||||
|
defining(params("id").toInt){ issueId =>
|
||||||
|
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||||
|
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
||||||
|
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
|
||||||
|
Ok("updated")
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
|
||||||
|
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
|
||||||
|
milestoneId("milestoneId").map { milestoneId =>
|
||||||
|
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||||
|
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
|
||||||
|
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
|
||||||
|
} getOrElse NotFound
|
||||||
|
} getOrElse Ok()
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||||
|
defining(params.get("value")){ action =>
|
||||||
|
action match {
|
||||||
|
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
|
||||||
|
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
|
||||||
|
case _ => // TODO BadRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
RawData(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: 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
|
||||||
|
params("from") match {
|
||||||
|
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||||
|
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 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) flatMap { issue =>
|
||||||
|
val (action, recordActivity) =
|
||||||
|
getAction(issue)
|
||||||
|
.collect {
|
||||||
|
case "close" if(!issue.closed) => true ->
|
||||||
|
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||||
|
case "reopen" if(issue.closed) => false ->
|
||||||
|
(Some("reopen") -> Some(recordReopenIssueActivity _))
|
||||||
|
}
|
||||||
|
.map { case (closed, t) =>
|
||||||
|
updateClosed(owner, name, issueId, closed)
|
||||||
|
t
|
||||||
|
}
|
||||||
|
.getOrElse(None -> None)
|
||||||
|
|
||||||
|
val commentId = (content, action) match {
|
||||||
|
case (None, None) => None
|
||||||
|
case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
|
||||||
|
case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// record comment activity if comment is entered
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call web hooks
|
||||||
|
action match {
|
||||||
|
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
|
||||||
|
case Some(act) => val webHookAction = act match {
|
||||||
|
case "open" => "opened"
|
||||||
|
case "reopen" => "reopened"
|
||||||
|
case "close" => "closed"
|
||||||
|
case _ => act
|
||||||
|
}
|
||||||
|
if(issue.isPullRequest){
|
||||||
|
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
|
||||||
|
} else {
|
||||||
|
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.get}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action foreach {
|
||||||
|
f.toNotify(repository, issueId, _){
|
||||||
|
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commentId.map( issue -> _ )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
|
||||||
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
val sessionKey = Keys.Session.Issues(owner, repoName)
|
||||||
|
|
||||||
|
// retrieve search condition
|
||||||
|
val condition = session.putAndGet(sessionKey,
|
||||||
|
if(request.hasQueryString){
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
if(q == null || q.trim.isEmpty){
|
||||||
|
IssueSearchCondition(request)
|
||||||
|
} else {
|
||||||
|
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
|
||||||
|
}
|
||||||
|
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||||
|
)
|
||||||
|
|
||||||
|
html.list(
|
||||||
|
"issues",
|
||||||
|
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||||
|
page,
|
||||||
|
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||||
|
getMilestones(owner, repoName),
|
||||||
|
getLabels(owner, repoName),
|
||||||
|
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
|
||||||
|
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
|
||||||
|
condition,
|
||||||
|
repository,
|
||||||
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.issues.labels.html
|
||||||
|
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
||||||
|
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
import org.scalatra.Ok
|
||||||
|
|
||||||
|
class LabelsController extends LabelsControllerBase
|
||||||
|
with LabelsService with IssuesService with RepositoryService with AccountService
|
||||||
|
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
|
trait LabelsControllerBase extends ControllerBase {
|
||||||
|
self: LabelsService with IssuesService with RepositoryService
|
||||||
|
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
|
case class LabelForm(labelName: String, color: String)
|
||||||
|
|
||||||
|
val labelForm = mapping(
|
||||||
|
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||||
|
"labelColor" -> trim(label("Color", text(required, color)))
|
||||||
|
)(LabelForm.apply)
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||||
|
html.list(
|
||||||
|
getLabels(repository.owner, repository.name),
|
||||||
|
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||||
|
repository,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
||||||
|
html.edit(None, repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
|
||||||
|
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
||||||
|
html.label(
|
||||||
|
getLabel(repository.owner, repository.name, labelId).get,
|
||||||
|
// TODO futility
|
||||||
|
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||||
|
repository,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
||||||
|
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
||||||
|
html.edit(Some(label), repository)
|
||||||
|
} getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
|
||||||
|
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
|
||||||
|
html.label(
|
||||||
|
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
|
||||||
|
// TODO futility
|
||||||
|
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||||
|
repository,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
|
||||||
|
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
||||||
|
Ok()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package app
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.issues.milestones.html
|
||||||
|
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
|
||||||
|
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
import service._
|
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
|
||||||
|
|
||||||
class MilestonesController extends MilestonesControllerBase
|
class MilestonesController extends MilestonesControllerBase
|
||||||
with MilestonesService with RepositoryService with AccountService
|
with MilestonesService with RepositoryService with AccountService
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
@@ -22,7 +23,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
|||||||
)(MilestoneForm.apply)
|
)(MilestoneForm.apply)
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones")(referrersOnly { repository =>
|
get("/:owner/:repository/issues/milestones")(referrersOnly { repository =>
|
||||||
issues.milestones.html.list(
|
html.list(
|
||||||
params.getOrElse("state", "open"),
|
params.getOrElse("state", "open"),
|
||||||
getMilestonesWithIssueCount(repository.owner, repository.name),
|
getMilestonesWithIssueCount(repository.owner, repository.name),
|
||||||
repository,
|
repository,
|
||||||
@@ -30,7 +31,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly {
|
get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly {
|
||||||
issues.milestones.html.edit(None, _)
|
html.edit(None, _)
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
||||||
@@ -39,34 +40,44 @@ trait MilestonesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
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 =>
|
||||||
|
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 =>
|
||||||
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
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 =>
|
||||||
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
closeMilestone(milestone)
|
closeMilestone(milestone)
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
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 =>
|
||||||
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
openMilestone(milestone)
|
openMilestone(milestone)
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
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 =>
|
||||||
|
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
|
||||||
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
|
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
|
||||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.api._
|
||||||
|
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
|
||||||
|
import gitbucket.core.pulls.html
|
||||||
|
import gitbucket.core.service.CommitStatusService
|
||||||
|
import gitbucket.core.service.MergeService
|
||||||
|
import gitbucket.core.service.IssuesService._
|
||||||
|
import gitbucket.core.service.PullRequestService._
|
||||||
|
import gitbucket.core.service._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.JGitUtil._
|
||||||
|
import gitbucket.core.util._
|
||||||
|
import gitbucket.core.view
|
||||||
|
import gitbucket.core.view.helpers
|
||||||
|
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.lib.PersonIdent
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
|
|
||||||
|
class PullRequestsController extends PullRequestsControllerBase
|
||||||
|
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||||
|
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
with CommitStatusService with MergeService
|
||||||
|
|
||||||
|
|
||||||
|
trait PullRequestsControllerBase extends ControllerBase {
|
||||||
|
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||||
|
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
with CommitStatusService with MergeService =>
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
if(Option(q).exists(_.contains("is:issue"))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
|
||||||
|
} else {
|
||||||
|
searchPullRequests(None, repository)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#list-pull-requests
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
// TODO: more api spec condition
|
||||||
|
val condition = IssueSearchCondition(request)
|
||||||
|
val baseOwner = getAccountByUserName(repository.owner).get
|
||||||
|
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
|
||||||
|
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
|
||||||
|
ApiPullRequest(
|
||||||
|
issue,
|
||||||
|
pullRequest,
|
||||||
|
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||||
|
ApiRepository(repository, ApiUser(baseOwner)),
|
||||||
|
ApiUser(issueUser)) })
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
html.pullreq(
|
||||||
|
issue, pullreq,
|
||||||
|
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
|
||||||
|
.sortWith((a, b) => a.registeredDate before b.registeredDate),
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||||
|
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.userName), Set())
|
||||||
|
baseOwner <- users.get(repository.owner)
|
||||||
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
|
issueUser <- users.get(issue.userName)
|
||||||
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiPullRequest(
|
||||||
|
issue,
|
||||||
|
pullRequest,
|
||||||
|
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||||
|
ApiRepository(repository, ApiUser(baseOwner)),
|
||||||
|
ApiUser(issueUser)))
|
||||||
|
}).getOrElse(NotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
|
||||||
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
|
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||||
|
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
|
||||||
|
val newId = git.getRepository.resolve(pullreq.commitIdTo)
|
||||||
|
val repoFullName = RepositoryName(repository)
|
||||||
|
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
|
||||||
|
JsonFormat(commits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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) =>
|
||||||
|
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo)
|
||||||
|
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
|
||||||
|
checkConflict(owner, name, pullreq.branch, issueId)
|
||||||
|
}
|
||||||
|
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
|
||||||
|
html.mergeguide(
|
||||||
|
hasConfrict,
|
||||||
|
hasProblem,
|
||||||
|
issue,
|
||||||
|
pullreq,
|
||||||
|
statuses,
|
||||||
|
repository,
|
||||||
|
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 git repository
|
||||||
|
mergePullRequest(git, pullreq.branch, issueId,
|
||||||
|
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
|
||||||
|
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||||
|
|
||||||
|
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
|
||||||
|
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||||
|
|
||||||
|
// 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, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||||
|
val (forkedOwner, forkedId) = 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 (oldId, newId) =
|
||||||
|
if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||||
|
// Branch name
|
||||||
|
val rootId = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||||
|
originRepository.owner, originRepository.name, originId,
|
||||||
|
forkedRepository.owner, forkedRepository.name, forkedId)
|
||||||
|
|
||||||
|
(oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId))
|
||||||
|
} else {
|
||||||
|
// Commit id
|
||||||
|
(oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId))
|
||||||
|
}
|
||||||
|
|
||||||
|
val (commits, diffs) = getRequestCompareInfo(
|
||||||
|
originRepository.owner, originRepository.name, oldId.getName,
|
||||||
|
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||||
|
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||||
|
originId,
|
||||||
|
forkedId,
|
||||||
|
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
|
||||||
|
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}"){
|
||||||
|
checkConflict(originRepository.owner, originRepository.name, originBranch,
|
||||||
|
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||||
|
}
|
||||||
|
html.mergecheck(conflict)
|
||||||
|
}
|
||||||
|
}) 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
|
||||||
|
fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
|
||||||
|
|
||||||
|
// call web hook
|
||||||
|
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||||
|
|
||||||
|
// 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}")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) =>
|
||||||
|
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 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())
|
||||||
|
)
|
||||||
|
|
||||||
|
gitbucket.core.issues.html.list(
|
||||||
|
"pulls",
|
||||||
|
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||||
|
page,
|
||||||
|
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||||
|
getMilestones(owner, repoName),
|
||||||
|
getLabels(owner, repoName),
|
||||||
|
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
|
||||||
|
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
|
||||||
|
condition,
|
||||||
|
repository,
|
||||||
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.settings.html
|
||||||
|
import gitbucket.core.model.WebHook
|
||||||
|
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
|
||||||
|
import gitbucket.core.service.WebHookService._
|
||||||
|
import gitbucket.core.util._
|
||||||
|
import gitbucket.core.util.JGitUtil._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
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.Constants
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
html.options(_, flash.get("info"))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the repository options.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
||||||
|
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
|
||||||
|
saveRepositoryOptions(
|
||||||
|
repository.owner,
|
||||||
|
repository.name,
|
||||||
|
form.description,
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Change repository HEAD
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
|
||||||
|
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
|
||||||
|
}
|
||||||
|
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 =>
|
||||||
|
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 =>
|
||||||
|
html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), 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.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, 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(_))
|
||||||
|
|
||||||
|
getAccountByUserName(repository.owner).foreach { ownerAccount =>
|
||||||
|
callWebHook("push",
|
||||||
|
List(WebHook(repository.owner, repository.name, form.url)),
|
||||||
|
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
flash += "url" -> form.url
|
||||||
|
flash += "info" -> "Test payload deployed!"
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the danger zone.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/settings/danger")(ownerOnly {
|
||||||
|
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." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,643 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.api._
|
||||||
|
import gitbucket.core.repo.html
|
||||||
|
import gitbucket.core.helper
|
||||||
|
import gitbucket.core.service._
|
||||||
|
import gitbucket.core.util._
|
||||||
|
import gitbucket.core.util.JGitUtil._
|
||||||
|
import gitbucket.core.util.StringUtil._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.model.{Account, CommitState}
|
||||||
|
import gitbucket.core.service.CommitStatusService
|
||||||
|
import gitbucket.core.service.WebHookService._
|
||||||
|
import gitbucket.core.view
|
||||||
|
import gitbucket.core.view.helpers
|
||||||
|
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.eclipse.jgit.api.{ArchiveCommand, Git}
|
||||||
|
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.lib._
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
|
import org.eclipse.jgit.treewalk._
|
||||||
|
import org.scalatra._
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||||
|
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||||
|
with WebHookPullRequestService
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The repository viewer.
|
||||||
|
*/
|
||||||
|
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||||
|
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||||
|
with WebHookPullRequestService =>
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
case class CommentForm(
|
||||||
|
fileName: Option[String],
|
||||||
|
oldLineNumber: Option[Int],
|
||||||
|
newLineNumber: Option[Int],
|
||||||
|
content: String,
|
||||||
|
issueId: Option[Int]
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
val commentForm = mapping(
|
||||||
|
"fileName" -> trim(label("Filename", optional(text()))),
|
||||||
|
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
|
||||||
|
"newLineNumber" -> trim(label("New line number", optional(number()))),
|
||||||
|
"content" -> trim(label("Content", text(required))),
|
||||||
|
"issueId" -> trim(label("Issue Id", optional(number())))
|
||||||
|
)(CommentForm.apply)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns converted HTML from Markdown for preview.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/_preview")(referrersOnly { repository =>
|
||||||
|
contentType = "text/html"
|
||||||
|
helpers.markdown(params("content"), repository,
|
||||||
|
params("enableWikiLink").toBoolean,
|
||||||
|
params("enableRefsLink").toBoolean,
|
||||||
|
params("enableTaskList").toBoolean,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the file list of the repository root and the default branch.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository")(referrersOnly {
|
||||||
|
fileList(_)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/#get
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the file list of the specified path and branch.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
|
||||||
|
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
if(path.isEmpty){
|
||||||
|
fileList(repository, id)
|
||||||
|
} else {
|
||||||
|
fileList(repository, id, path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the commit list of the specified resource.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
|
||||||
|
val (branchName, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
|
||||||
|
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
||||||
|
case Right((logs, hasNext)) =>
|
||||||
|
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
||||||
|
logs.splitWith{ (commit1, commit2) =>
|
||||||
|
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||||
|
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
case Left(_) => NotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("sha")
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
data <- extractFromJsonBody[CreateAStatus] if data.isValid
|
||||||
|
creator <- context.loginAccount
|
||||||
|
state <- CommitState.valueOf(data.state)
|
||||||
|
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
|
||||||
|
state, data.target_url, data.description, new java.util.Date(), creator)
|
||||||
|
status <- getCommitStatus(repository.owner, repository.name, statusId)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("ref")
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
|
||||||
|
ApiCommitStatus(status, ApiUser(creator))
|
||||||
|
})
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("ref")
|
||||||
|
owner <- getAccountByUserName(repository.owner)
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
} yield {
|
||||||
|
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
|
||||||
|
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||||
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
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("/")
|
||||||
|
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("/")
|
||||||
|
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 = repository,
|
||||||
|
branch = form.branch,
|
||||||
|
path = form.path,
|
||||||
|
newFileName = Some(form.newFileName),
|
||||||
|
oldFileName = None,
|
||||||
|
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||||
|
charset = form.charset,
|
||||||
|
message = 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 = repository,
|
||||||
|
branch = form.branch,
|
||||||
|
path = form.path,
|
||||||
|
newFileName = Some(form.newFileName),
|
||||||
|
oldFileName = form.oldFileName,
|
||||||
|
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||||
|
charset = form.charset,
|
||||||
|
message = 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.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
||||||
|
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
val raw = params.get("raw").getOrElse("false").toBoolean
|
||||||
|
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||||
|
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
|
||||||
|
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||||
|
if(raw){
|
||||||
|
// Download
|
||||||
|
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
|
||||||
|
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
|
||||||
|
new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays details of the specified commit.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
|
||||||
|
val id = params("id")
|
||||||
|
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
|
||||||
|
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
||||||
|
html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||||
|
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||||
|
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||||
|
getCommitComments(repository.owner, repository.name, id, false),
|
||||||
|
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
val id = params("id")
|
||||||
|
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
|
||||||
|
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
|
||||||
|
form.issueId match {
|
||||||
|
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||||
|
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
|
||||||
|
val id = params("id")
|
||||||
|
val fileName = params.get("fileName")
|
||||||
|
val oldLineNumber = params.get("oldLineNumber") map (_.toInt)
|
||||||
|
val newLineNumber = params.get("newLineNumber") map (_.toInt)
|
||||||
|
val issueId = params.get("issueId") map (_.toInt)
|
||||||
|
html.commentform(
|
||||||
|
commitId = id,
|
||||||
|
fileName, oldLineNumber, newLineNumber, issueId,
|
||||||
|
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||||
|
repository = repository
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
val id = params("id")
|
||||||
|
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
|
||||||
|
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
|
||||||
|
form.issueId match {
|
||||||
|
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||||
|
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
|
}
|
||||||
|
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
|
||||||
|
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
|
||||||
|
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
||||||
|
params.get("dataType") collect {
|
||||||
|
case t if t == "html" => html.editcomment(
|
||||||
|
x.content, x.commentId, x.userName, x.repositoryName)
|
||||||
|
} getOrElse {
|
||||||
|
contentType = formats("json")
|
||||||
|
org.json4s.jackson.Serialization.write(
|
||||||
|
Map("content" -> view.Markdown.toHtml(x.content,
|
||||||
|
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getCommitComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
updateCommitComment(comment.commentId, form.content)
|
||||||
|
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getCommitComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
Ok(deleteCommitComment(comment.commentId))
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays branches.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||||
|
val branches = JGitUtil.getBranches(repository.owner, repository.name, repository.repository.defaultBranch)
|
||||||
|
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
||||||
|
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
|
||||||
|
.reverse
|
||||||
|
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a branch.
|
||||||
|
*/
|
||||||
|
post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
|
||||||
|
val newBranchName = params.getOrElse("new", halt(400))
|
||||||
|
val fromBranchName = params.getOrElse("from", halt(400))
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
JGitUtil.createBranch(git, fromBranchName, newBranchName)
|
||||||
|
} match {
|
||||||
|
case Right(message) =>
|
||||||
|
flash += "info" -> message
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}")
|
||||||
|
case Left(message) =>
|
||||||
|
flash += "error" -> message
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/tags")(referrersOnly {
|
||||||
|
html.tags(_)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download repository contents as an archive.
|
||||||
|
*/
|
||||||
|
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
|
||||||
|
multiParams("splat").head match {
|
||||||
|
case name if name.endsWith(".zip") =>
|
||||||
|
archiveRepository(name, ".zip", repository)
|
||||||
|
case name if name.endsWith(".tar.gz") =>
|
||||||
|
archiveRepository(name, ".tar.gz", repository)
|
||||||
|
case _ => BadRequest
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/network/members")(referrersOnly { repository =>
|
||||||
|
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: 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.
|
||||||
|
*
|
||||||
|
* @param repository the repository information
|
||||||
|
* @param revstr the branch name or commit id(optional)
|
||||||
|
* @param path the directory path (optional)
|
||||||
|
* @return HTML of the file list
|
||||||
|
*/
|
||||||
|
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
|
||||||
|
if(repository.commitCount == 0){
|
||||||
|
html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
|
} else {
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
// get specified commit
|
||||||
|
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
|
||||||
|
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
|
||||||
|
val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
html.files(revision, repository,
|
||||||
|
if(path == ".") Nil else path.split("/").toList, // current path
|
||||||
|
context.loginAccount match {
|
||||||
|
case None => List()
|
||||||
|
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||||
|
}, // groups of current user
|
||||||
|
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
||||||
|
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||||
|
flash.get("info"), flash.get("error"))
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def commitFile(repository: RepositoryService.RepositoryInfo,
|
||||||
|
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
|
||||||
|
content: String, charset: String, message: String) = {
|
||||||
|
|
||||||
|
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}" }
|
||||||
|
|
||||||
|
LockUtil.lock(s"${repository.owner}/${repository.name}"){
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
|
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(headName)
|
||||||
|
|
||||||
|
JGitUtil.processTree(git, headTip){ (path, tree) =>
|
||||||
|
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newPath.foreach { newPath =>
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
|
||||||
|
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
|
||||||
|
}
|
||||||
|
builder.finish()
|
||||||
|
|
||||||
|
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
|
||||||
|
headName, loginAccount.fullName, loginAccount.mailAddress, message)
|
||||||
|
|
||||||
|
inserter.flush()
|
||||||
|
inserter.release()
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// update pull request
|
||||||
|
updatePullRequests(repository.owner, repository.name, branch)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
|
||||||
|
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||||
|
callWebHookOf(repository.owner, repository.name, "push") {
|
||||||
|
getAccountByUserName(repository.owner).map{ ownerAccount =>
|
||||||
|
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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): Unit = {
|
||||||
|
val revision = name.stripSuffix(suffix)
|
||||||
|
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
||||||
|
if(workDir.exists) {
|
||||||
|
FileUtils.deleteDirectory(workDir)
|
||||||
|
}
|
||||||
|
workDir.mkdirs
|
||||||
|
|
||||||
|
val filename = 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))
|
||||||
|
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
|
||||||
|
response.setBufferSize(1024 * 1024);
|
||||||
|
|
||||||
|
git.archive
|
||||||
|
.setFormat(suffix.tail)
|
||||||
|
.setTree(revCommit.getTree)
|
||||||
|
.setOutputStream(response.getOutputStream)
|
||||||
|
.call()
|
||||||
|
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
|
||||||
|
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.search.html
|
||||||
|
import gitbucket.core.service._
|
||||||
|
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
|
||||||
|
import ControlUtil._
|
||||||
|
import Implicits._
|
||||||
|
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" => html.issues(
|
||||||
|
searchIssues(repository.owner, repository.name, query),
|
||||||
|
countFiles(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
|
||||||
|
case _ => html.code(
|
||||||
|
searchFiles(repository.owner, repository.name, query),
|
||||||
|
countIssues(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.admin.html
|
||||||
|
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
||||||
|
import gitbucket.core.util.AdminAuthenticator
|
||||||
|
import gitbucket.core.ssh.SshServer
|
||||||
|
import SystemSettingsService._
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
|
with AccountService with AdminAuthenticator
|
||||||
|
|
||||||
|
trait SystemSettingsControllerBase extends ControllerBase {
|
||||||
|
self: AccountService with AdminAuthenticator =>
|
||||||
|
|
||||||
|
private val form = mapping(
|
||||||
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
|
"information" -> trim(label("Information", optional(text()))),
|
||||||
|
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||||
|
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
|
||||||
|
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", 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()))),
|
||||||
|
"ssl" -> trim(label("Enable SSL", 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 {
|
||||||
|
html.system(flash.get("info"))
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/system", form)(adminOnly { form =>
|
||||||
|
saveSystemSettings(form)
|
||||||
|
|
||||||
|
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
|
||||||
|
SshServer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
|
||||||
|
SshServer.start(
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user