mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-07 23:25:51 +02:00
Compare commits
927 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c73e89ccd4 | ||
|
|
f58a506780 | ||
|
|
411d19e74e | ||
|
|
bd51ffd9d2 | ||
|
|
83980fdccd | ||
|
|
baab243bc8 | ||
|
|
4dd8e1dc63 | ||
|
|
0b11b8b084 | ||
|
|
704775dc60 | ||
|
|
c7a7be1de0 | ||
|
|
e21a970977 | ||
|
|
a526dcf2dd | ||
|
|
e86710fbbd | ||
|
|
73e850493a | ||
|
|
7d735f6f8a | ||
|
|
043e99a9eb | ||
|
|
65e079b1d3 | ||
|
|
32e6f584d8 | ||
|
|
ebc5219ce6 | ||
|
|
ad8e620bdf | ||
|
|
8590c693b9 | ||
|
|
8b29bf7d93 | ||
|
|
43b7f83082 | ||
|
|
9ef5c981ef | ||
|
|
f027ac34d4 | ||
|
|
2ea31d6869 | ||
|
|
a1af7d0f9c | ||
|
|
797bf37bfa | ||
|
|
7f78815c11 | ||
|
|
d8a3f308ed | ||
|
|
74e18a982d | ||
|
|
e86ad423c5 | ||
|
|
2f00060c57 | ||
|
|
3b7e6aa68e | ||
|
|
5f3d6242fd | ||
|
|
6793a86bae | ||
|
|
d99ce20529 | ||
|
|
93e7b604cd | ||
|
|
5c81ce9b68 | ||
|
|
c9cf62701f | ||
|
|
f65babca4b | ||
|
|
23c146fc5d | ||
|
|
e14f336142 | ||
|
|
54647be5bd | ||
|
|
9517d65646 | ||
|
|
0156e401fa | ||
|
|
d6e2bc464d | ||
|
|
5124ff593d | ||
|
|
727c90afdc | ||
|
|
9ebd5e3265 | ||
|
|
d120142127 | ||
|
|
f9e4cddcaf | ||
|
|
8f64f174d9 | ||
|
|
d6817796b3 | ||
|
|
ab19d473c4 | ||
|
|
48f0116358 | ||
|
|
d8e6e97845 | ||
|
|
9a8920788c | ||
|
|
27864a3a3c | ||
|
|
b39e863591 | ||
|
|
7661e8cadd | ||
|
|
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 | ||
|
|
05a91565dc | ||
|
|
dc78dc9b0d |
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: scala
|
||||
scala:
|
||||
- 2.11.6
|
||||
165
README.md
165
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:
|
||||
|
||||
- Public / Private Git repository (http access only)
|
||||
- Repository viewer (some advanced features such as online file editing are not implemented)
|
||||
- Public / Private Git repository (http and ssh access)
|
||||
- Repository viewer and online file editing
|
||||
- Repository search (Code and Issues)
|
||||
- Wiki
|
||||
- Issues
|
||||
@@ -20,10 +23,8 @@ The current version of GitBucket provides a basic features below:
|
||||
|
||||
Following features are not implemented, but we will make them in the future release!
|
||||
|
||||
- File editing in repository viewer
|
||||
- Comment for the changeset
|
||||
- Network graph
|
||||
- Statics
|
||||
- Statistics
|
||||
- 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).
|
||||
@@ -35,21 +36,40 @@ Installation
|
||||
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.
|
||||
|
||||
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**.
|
||||
|
||||
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.
|
||||
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]
|
||||
- --https=true
|
||||
- --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.
|
||||
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
|
||||
@@ -59,6 +79,127 @@ Run the following commands in `Terminal` to
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 3.2 - 3 May 2015
|
||||
- Directory history button
|
||||
- Compare / pull request button
|
||||
- Limit of activity log
|
||||
|
||||
### 3.1.1 - 4 Apr 2015
|
||||
- Rolled back H2 version to avoid version compatibility issue
|
||||
- Plug-ins became possible to access ServletContext
|
||||
|
||||
### 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
|
||||
@@ -74,8 +215,8 @@ Release Notes
|
||||
|
||||
### 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 `--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
|
||||
|
||||
10
build.xml
10
build.xml
@@ -4,9 +4,9 @@
|
||||
<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.10"/>
|
||||
<property name="gitbucket.version" value="0.0.1"/>
|
||||
<property name="jetty.version" value="8.1.8.v20121106"/>
|
||||
<property name="scala.version" value="2.11"/>
|
||||
<property name="gitbucket.version" value="3.2.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">
|
||||
@@ -50,8 +50,8 @@
|
||||
</target>
|
||||
|
||||
<target name="rename" depends="embed">
|
||||
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||
<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">
|
||||
|
||||
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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# /etc/rc.d/init.d/gitbucket
|
||||
# RedHat: /etc/rc.d/init.d/gitbucket
|
||||
# Ubuntu: /etc/init.d/gitbucket
|
||||
# Mac OS/X: /Library/StartupItems/GitBucket
|
||||
#
|
||||
# Starts the GitBucket server
|
||||
#
|
||||
@@ -8,28 +10,44 @@
|
||||
# description: Run GitBucket server
|
||||
# processname: java
|
||||
|
||||
# Source function library
|
||||
. /etc/rc.d/init.d/functions
|
||||
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 ] && . /etc/sysconfig/gitbucket
|
||||
[ -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=/var/log/gitbucket/run.log
|
||||
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
|
||||
PID_FILE=/var/run/gitbucket.pid
|
||||
|
||||
# Default return value
|
||||
RETVAL=0
|
||||
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: "
|
||||
|
||||
# Compile statup parameters
|
||||
START_OPTS=
|
||||
if [ $GITBUCKET_PORT ]; then
|
||||
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
|
||||
fi
|
||||
@@ -39,21 +57,16 @@ start() {
|
||||
if [ $GITBUCKET_HOST ]; then
|
||||
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
|
||||
fi
|
||||
if [ $GITBUCKET_HTTPS ]; then
|
||||
START_OPTS="${START_OPTS} --https=true"
|
||||
fi
|
||||
|
||||
# Run the Java process
|
||||
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
|
||||
RETVAL=$?
|
||||
|
||||
# Store PID of the Java process into a file
|
||||
echo $! > $PID_FILE
|
||||
|
||||
if [ $RETVAL -eq 0 ] ; then
|
||||
success "GitBucket startup"
|
||||
success "Success"
|
||||
else
|
||||
failure "GitBucket startup"
|
||||
failure "Exit code $RETVAL"
|
||||
fi
|
||||
|
||||
echo
|
||||
@@ -85,25 +98,41 @@ restart() {
|
||||
start
|
||||
}
|
||||
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
## MacOS proxies for System V service hooks:
|
||||
StartService() {
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
}
|
||||
|
||||
StopService() {
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
}
|
||||
|
||||
RestartService() {
|
||||
restart
|
||||
;;
|
||||
status)
|
||||
status -p $PID_FILE java
|
||||
RETVAL=$?
|
||||
;;
|
||||
*)
|
||||
echo $"Usage: $0 [start|stop|restart|status]"
|
||||
RETVAL=2
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
exit $RETVAL
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: gitbucket
|
||||
Summary: GitHub clone written with Scala.
|
||||
Version: 1.7
|
||||
Version: 2.6
|
||||
Release: 1%{?dist}
|
||||
License: Apache
|
||||
URL: https://github.com/takezoe/gitbucket
|
||||
@@ -26,6 +26,25 @@ GitBucket is the easily installable GitHub clone written with Scala.
|
||||
%{__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}"
|
||||
@@ -34,12 +53,28 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%{_datarootdir}/%{name}/lib/%{name}.war
|
||||
%{_sysconfdir}/init.d/%{name}
|
||||
%config %{_sysconfdir}/sysconfig/%{name}
|
||||
%{_localstatedir}/log/%{name}/run.log
|
||||
%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.
|
||||
|
||||
14
contrib/macosx/gitbucket.plist → contrib/macosx/makePlist
Normal file → Executable file
14
contrib/macosx/gitbucket.plist → contrib/macosx/makePlist
Normal file → Executable file
@@ -1,3 +1,10 @@
|
||||
#!/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">
|
||||
@@ -7,14 +14,15 @@
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/java</string>
|
||||
<string>-Dmail.smtp.starttls.enable=true</string>
|
||||
<string>$GITBUCKET_JVM_OPTS</string>
|
||||
<string>-jar</string>
|
||||
<string>gitbucket.war</string>
|
||||
<string>--host=127.0.0.1</string>
|
||||
<string>--port=8080</string>
|
||||
<string>--host=$GITBUCKET_HOST</string>
|
||||
<string>--port=$GITBUCKET_PORT</string>
|
||||
<string>--https=true</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
@@ -1,20 +0,0 @@
|
||||
# Bind host
|
||||
#GITBUCKET_HOST=0.0.0.0
|
||||
|
||||
# Server port
|
||||
#GITBUCKET_PORT=8080
|
||||
|
||||
# Force HTTPS scheme
|
||||
#GITBUCKET_HTTPS=false
|
||||
|
||||
# Data directory (GITBUCKET_HOME/gitbucket)
|
||||
#GITBUCKET_HOME=/var/lib/gitbucket
|
||||
|
||||
# Path to the WAR file
|
||||
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||
|
||||
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
|
||||
#GITBUCKET_PREFIX=
|
||||
|
||||
# Other Java option
|
||||
#GITBUCKET_JVM_OPTS=
|
||||
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
|
||||
* /repositories
|
||||
* /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));
|
||||
});
|
||||
});
|
||||
```
|
||||
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.
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.
Binary file not shown.
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.
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.
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.
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.
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.
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.1.1\
|
||||
-Dpackaging=jar\
|
||||
-Dfile=../target/scala-2.11/gitbucket-assembly-3.2.0.jar\
|
||||
-DrepositoryId=sourceforge.jp\
|
||||
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
|
||||
1495
etc/icons.svg
1495
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 +1 @@
|
||||
sbt.version=0.12.3
|
||||
sbt.version=0.13.8
|
||||
|
||||
@@ -1,53 +1,80 @@
|
||||
import sbt._
|
||||
import Keys._
|
||||
import org.scalatra.sbt._
|
||||
import twirl.sbt.TwirlPlugin._
|
||||
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 {
|
||||
val Organization = "jp.sf.amateras"
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "0.0.1"
|
||||
val ScalaVersion = "2.10.3"
|
||||
val ScalatraVersion = "2.2.1"
|
||||
val Version = "3.2.0"
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
|
||||
lazy val project = Project (
|
||||
"gitbucket",
|
||||
file("."),
|
||||
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
|
||||
organization := Organization,
|
||||
name := Name,
|
||||
version := Version,
|
||||
scalaVersion := ScalaVersion,
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||
),
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.2.5",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"org.pegdown" % "pegdown" % "1.4.1",
|
||||
"org.apache.commons" % "commons-compress" % "1.5",
|
||||
"org.apache.commons" % "commons-email" % "1.3.1",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.3",
|
||||
"com.typesafe.slick" %% "slick" % "1.0.1",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.3.173",
|
||||
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||
"junit" % "junit" % "4.11" % "test"
|
||||
),
|
||||
EclipseKeys.withSource := true,
|
||||
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
|
||||
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
|
||||
packageOptions += Package.MainClass("JettyLauncher")
|
||||
) ++ seq(Twirl.settings: _*)
|
||||
file(".")
|
||||
)
|
||||
.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,
|
||||
name := Name,
|
||||
version := Version,
|
||||
scalaVersion := ScalaVersion,
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||
),
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||
libraryDependencies ++= Seq(
|
||||
"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-specs2" % ScalatraVersion % "test",
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.2.11",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"org.pegdown" % "pegdown" % "1.4.1", // 1.4.2 has incompatible APi changes
|
||||
"org.apache.commons" % "commons-compress" % "1.9",
|
||||
"org.apache.commons" % "commons-email" % "1.3.3",
|
||||
"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.h2database" % "h2" % "1.4.180",
|
||||
// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||
"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"),
|
||||
"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",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x"
|
||||
),
|
||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
|
||||
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,9 +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("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
|
||||
|
||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")
|
||||
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.5")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
|
||||
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
|
||||
java -Dsbt.log.noformat=true -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 -Dsbt.log.noformat=true -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 "$@"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
import org.eclipse.jetty.webapp.WebAppContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.security.ProtectionDomain;
|
||||
|
||||
@@ -25,8 +23,6 @@ public class JettyLauncher {
|
||||
port = Integer.parseInt(dim[1]);
|
||||
} else if(dim[0].equals("--prefix")) {
|
||||
contextPath = dim[1];
|
||||
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
|
||||
forceHttps = true;
|
||||
} else if(dim[0].equals("--gitbucket.home")){
|
||||
System.setProperty("gitbucket.home", dim[1]);
|
||||
}
|
||||
@@ -36,7 +32,7 @@ public class JettyLauncher {
|
||||
|
||||
Server server = new Server();
|
||||
|
||||
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
|
||||
SelectChannelConnector connector = new SelectChannelConnector();
|
||||
if(host != null) {
|
||||
connector.setHost(host);
|
||||
}
|
||||
@@ -46,6 +42,14 @@ public class JettyLauncher {
|
||||
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();
|
||||
|
||||
@@ -53,25 +57,35 @@ public class JettyLauncher {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
class HttpsSupportConnector extends SelectChannelConnector {
|
||||
private boolean forceHttps;
|
||||
|
||||
public HttpsSupportConnector(boolean forceHttps) {
|
||||
this.forceHttps = forceHttps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(final EndPoint endpoint, final Request request) throws IOException {
|
||||
if (this.forceHttps) {
|
||||
request.setScheme("https");
|
||||
super.customize(endpoint, request);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package util;
|
||||
package gitbucket.core.util;
|
||||
|
||||
import org.eclipse.jgit.api.errors.PatchApplyException;
|
||||
import org.eclipse.jgit.diff.RawText;
|
||||
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,135 +1,135 @@
|
||||
CREATE TABLE ACCOUNT(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
MAIL_ADDRESS VARCHAR(100) NOT NULL,
|
||||
PASSWORD VARCHAR(40) NOT NULL,
|
||||
ADMINISTRATOR BOOLEAN NOT NULL,
|
||||
URL VARCHAR(200),
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||
LAST_LOGIN_DATE TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE REPOSITORY(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
PRIVATE BOOLEAN NOT NULL,
|
||||
DESCRIPTION TEXT,
|
||||
DEFAULT_BRANCH VARCHAR(100),
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE COLLABORATOR(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
COLLABORATOR_NAME VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL,
|
||||
OPENED_USER_NAME VARCHAR(100) NOT NULL,
|
||||
MILESTONE_ID INT,
|
||||
ASSIGNED_USER_NAME VARCHAR(100),
|
||||
TITLE TEXT NOT NULL,
|
||||
CONTENT TEXT,
|
||||
CLOSED BOOLEAN NOT NULL,
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE_ID(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE_COMMENT(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL,
|
||||
COMMENT_ID INT AUTO_INCREMENT,
|
||||
ACTION VARCHAR(10),
|
||||
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
||||
CONTENT TEXT NOT NULL,
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE LABEL(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
LABEL_ID INT AUTO_INCREMENT,
|
||||
LABEL_NAME VARCHAR(100) NOT NULL,
|
||||
COLOR CHAR(6) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE_LABEL(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL,
|
||||
LABEL_ID INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE MILESTONE(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
MILESTONE_ID INT AUTO_INCREMENT,
|
||||
TITLE VARCHAR(100) NOT NULL,
|
||||
DESCRIPTION TEXT,
|
||||
DUE_DATE TIMESTAMP,
|
||||
CLOSED_DATE TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
|
||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
|
||||
|
||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
|
||||
|
||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
|
||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
|
||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||
|
||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
|
||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
|
||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
|
||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||
|
||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
|
||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
|
||||
INSERT INTO ACCOUNT (
|
||||
USER_NAME,
|
||||
MAIL_ADDRESS,
|
||||
PASSWORD,
|
||||
ADMINISTRATOR,
|
||||
URL,
|
||||
REGISTERED_DATE,
|
||||
UPDATED_DATE,
|
||||
LAST_LOGIN_DATE
|
||||
) VALUES (
|
||||
'root',
|
||||
'root@localhost',
|
||||
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
||||
true,
|
||||
'https://github.com/takezoe/gitbucket',
|
||||
SYSDATE,
|
||||
SYSDATE,
|
||||
NULL
|
||||
);
|
||||
CREATE TABLE ACCOUNT(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
MAIL_ADDRESS VARCHAR(100) NOT NULL,
|
||||
PASSWORD VARCHAR(40) NOT NULL,
|
||||
ADMINISTRATOR BOOLEAN NOT NULL,
|
||||
URL VARCHAR(200),
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||
LAST_LOGIN_DATE TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE REPOSITORY(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
PRIVATE BOOLEAN NOT NULL,
|
||||
DESCRIPTION TEXT,
|
||||
DEFAULT_BRANCH VARCHAR(100),
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE COLLABORATOR(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
COLLABORATOR_NAME VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL,
|
||||
OPENED_USER_NAME VARCHAR(100) NOT NULL,
|
||||
MILESTONE_ID INT,
|
||||
ASSIGNED_USER_NAME VARCHAR(100),
|
||||
TITLE TEXT NOT NULL,
|
||||
CONTENT TEXT,
|
||||
CLOSED BOOLEAN NOT NULL,
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE_ID(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE_COMMENT(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL,
|
||||
COMMENT_ID INT AUTO_INCREMENT,
|
||||
ACTION VARCHAR(10),
|
||||
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
||||
CONTENT TEXT NOT NULL,
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||
UPDATED_DATE TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE LABEL(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
LABEL_ID INT AUTO_INCREMENT,
|
||||
LABEL_NAME VARCHAR(100) NOT NULL,
|
||||
COLOR CHAR(6) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ISSUE_LABEL(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL,
|
||||
LABEL_ID INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE MILESTONE(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
MILESTONE_ID INT AUTO_INCREMENT,
|
||||
TITLE VARCHAR(100) NOT NULL,
|
||||
DESCRIPTION TEXT,
|
||||
DUE_DATE TIMESTAMP,
|
||||
CLOSED_DATE TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
|
||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
|
||||
|
||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
|
||||
|
||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
|
||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
|
||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||
|
||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
|
||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
|
||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
|
||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||
|
||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
|
||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
|
||||
INSERT INTO ACCOUNT (
|
||||
USER_NAME,
|
||||
MAIL_ADDRESS,
|
||||
PASSWORD,
|
||||
ADMINISTRATOR,
|
||||
URL,
|
||||
REGISTERED_DATE,
|
||||
UPDATED_DATE,
|
||||
LAST_LOGIN_DATE
|
||||
) VALUES (
|
||||
'root',
|
||||
'root@localhost',
|
||||
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
||||
true,
|
||||
'https://github.com/takezoe/gitbucket',
|
||||
SYSDATE,
|
||||
SYSDATE,
|
||||
NULL
|
||||
);
|
||||
|
||||
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;
|
||||
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,9 +1,14 @@
|
||||
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
|
||||
import app._
|
||||
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||
import org.scalatra._
|
||||
import javax.servlet._
|
||||
|
||||
import gitbucket.core.controller._
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, Database, TransactionFilter}
|
||||
import gitbucket.core.util.Directory
|
||||
|
||||
import java.util.EnumSet
|
||||
import javax.servlet._
|
||||
|
||||
import org.scalatra._
|
||||
|
||||
|
||||
class ScalatraBootstrap extends LifeCycle {
|
||||
override def init(context: ServletContext) {
|
||||
@@ -12,15 +17,21 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
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 SearchController, "/")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
context.mount(new DashboardController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
context.mount(new CreateRepositoryController, "/*")
|
||||
context.mount(new AccountController, "/*")
|
||||
context.mount(new RepositoryViewerController, "/*")
|
||||
context.mount(new WikiController, "/*")
|
||||
@@ -29,12 +40,15 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.mount(new IssuesController, "/*")
|
||||
context.mount(new PullRequestsController, "/*")
|
||||
context.mount(new RepositorySettingsController, "/*")
|
||||
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
|
||||
|
||||
// Create GITBUCKET_HOME directory if it does not exist
|
||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
||||
val dir = new java.io.File(Directory.GitBucketHome)
|
||||
if(!dir.exists){
|
||||
dir.mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def destroy(context: ServletContext): Unit = {
|
||||
Database.closeDataSource()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +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
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
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, 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)
|
||||
|
||||
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)
|
||||
|
||||
/**
|
||||
* 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),
|
||||
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("/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.fullName, form.mailAddress, false, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/signin")
|
||||
} else NotFound
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package app
|
||||
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import util._
|
||||
import service._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
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){
|
||||
using(Git.open(gitdir)){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
val content = if(form.description.nonEmpty){
|
||||
form.name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
form.description.get
|
||||
} else {
|
||||
form.name + "\n" +
|
||||
"===============\n"
|
||||
}
|
||||
|
||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||
}
|
||||
}
|
||||
|
||||
// Create Wiki repository
|
||||
createWikiRepository(loginAccount, form.owner, form.name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
|
||||
}
|
||||
|
||||
// redirect to the repository
|
||||
redirect(s"/${form.owner}/${form.name}")
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
|
||||
if(repository.owner == loginUserName){
|
||||
// redirect to the repository
|
||||
redirect(s"/${repository.owner}/${repository.name}")
|
||||
} else {
|
||||
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
|
||||
// redirect to the repository
|
||||
redirect(s"/${owner}/${name}")
|
||||
} getOrElse {
|
||||
// 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
|
||||
using(Git.open(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"/${loginUserName}/${repository.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
||||
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
||||
createLabel(userName, repositoryName, "enhancement", "84b6eb")
|
||||
createLabel(userName, repositoryName, "invalid", "e6e6e6")
|
||||
createLabel(userName, repositoryName, "question", "cc317c")
|
||||
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
||||
}
|
||||
|
||||
private def existsAccount: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate check for the repository name.
|
||||
*/
|
||||
private def unique: 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.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.{UsersAuthenticator, Keys}
|
||||
import util.Implicits._
|
||||
|
||||
class DashboardController extends DashboardControllerBase
|
||||
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||
with UsersAuthenticator
|
||||
|
||||
trait DashboardControllerBase extends ControllerBase {
|
||||
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
|
||||
|
||||
get("/dashboard/issues/repos")(usersOnly {
|
||||
searchIssues("all")
|
||||
})
|
||||
|
||||
get("/dashboard/issues/assigned")(usersOnly {
|
||||
searchIssues("assigned")
|
||||
})
|
||||
|
||||
get("/dashboard/issues/created_by")(usersOnly {
|
||||
searchIssues("created_by")
|
||||
})
|
||||
|
||||
get("/dashboard/pulls")(usersOnly {
|
||||
searchPullRequests("created_by", None)
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/owned")(usersOnly {
|
||||
searchPullRequests("created_by", None)
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/public")(usersOnly {
|
||||
searchPullRequests("not_created_by", None)
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
|
||||
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
|
||||
})
|
||||
|
||||
private def searchIssues(filter: String) = {
|
||||
import IssuesService._
|
||||
|
||||
// condition
|
||||
val condition = session.putAndGet(Keys.Session.DashboardIssues,
|
||||
if(request.hasQueryString) IssueSearchCondition(request)
|
||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
val userName = context.loginAccount.get.userName
|
||||
val 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 condition = session.putAndGet(Keys.Session.DashboardPulls, {
|
||||
if(request.hasQueryString) IssueSearchCondition(request)
|
||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
|
||||
}.copy(repo = repository))
|
||||
|
||||
val userName = context.loginAccount.get.userName
|
||||
val 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: _*)
|
||||
|
||||
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 _root_.util.{Keys, FileUtil}
|
||||
import util.ControlUtil._
|
||||
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)) => defining(generateFileId){ fileId =>
|
||||
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
|
||||
session += Keys.Session.Upload(fileId) -> file.name
|
||||
Ok(fileId)
|
||||
}
|
||||
case None => BadRequest
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import util.Implicits._
|
||||
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 =>
|
||||
|
||||
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
|
||||
|
||||
html.index(getRecentActivities(),
|
||||
getVisibleRepositories(loginAccount, baseUrl),
|
||||
loadSystemSettings(),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
|
||||
)
|
||||
}
|
||||
|
||||
get("/signin"){
|
||||
val redirect = params.get("redirect")
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
session.setAttribute(Keys.Session.Redirect, redirect.get)
|
||||
}
|
||||
html.signin(loadSystemSettings())
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
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(Keys.Session.LoginAccount, account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
|
||||
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
|
||||
redirect("/")
|
||||
} else {
|
||||
redirect(redirectUrl)
|
||||
}
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,65 +0,0 @@
|
||||
package app
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import service._
|
||||
import util.CollaboratorsAuthenticator
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
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, messages: Messages): 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,276 +0,0 @@
|
||||
package app
|
||||
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
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._
|
||||
import java.util.zip.{ZipEntry, ZipOutputStream}
|
||||
|
||||
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.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)) =>
|
||||
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
|
||||
|
||||
using(Git.open(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 objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
treeWalk.setRecursive(true)
|
||||
getPathObjectId(path, treeWalk)
|
||||
}
|
||||
|
||||
if(raw){
|
||||
// Download
|
||||
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
|
||||
contentType = FileUtil.getContentType(path, bytes)
|
||||
bytes
|
||||
}
|
||||
} 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")
|
||||
|
||||
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) =>
|
||||
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 =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
// retrieve latest update date of each branch
|
||||
val branchInfo = repository.branchList.map { branchName =>
|
||||
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
|
||||
(branchName, revCommit.getCommitterIdent.getWhen)
|
||||
}
|
||||
repo.html.branches(branchInfo, 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
|
||||
|
||||
val zipFile = new File(workDir, repository.name + "-" +
|
||||
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
|
||||
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||
using(new TreeWalk(git.getRepository)){ walk =>
|
||||
val reader = walk.getObjectReader
|
||||
val objectId = new MutableObjectId
|
||||
|
||||
using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
|
||||
walk.addTree(revCommit.getTree)
|
||||
walk.setRecursive(true)
|
||||
|
||||
while(walk.next){
|
||||
val name = walk.getPathString
|
||||
val mode = walk.getFileMode(0)
|
||||
if(mode != FileMode.TREE){
|
||||
walk.getObjectId(objectId, 0)
|
||||
val entry = new ZipEntry(name)
|
||||
val loader = reader.open(objectId)
|
||||
entry.setSize(loader.getSize)
|
||||
out.putNextEntry(entry)
|
||||
loader.copyTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentType = "application/octet-stream"
|
||||
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
|
||||
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("^/", ""))
|
||||
}
|
||||
|
||||
|
||||
private val readmeFiles = Seq("readme.md", "readme.markdown")
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
using(Git.open(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) =>
|
||||
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
// process README.md or README.markdown
|
||||
val readme = files.find { file =>
|
||||
readmeFiles.contains(file.name.toLowerCase)
|
||||
}.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,40 @@
|
||||
package app
|
||||
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 _root_.util.Directory._
|
||||
import _root_.util.Implicits._
|
||||
import _root_.util.ControlUtil._
|
||||
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
|
||||
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}
|
||||
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 Validations {
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
|
||||
with SystemSettingsService {
|
||||
|
||||
implicit val jsonFormats = DefaultFormats
|
||||
|
||||
// Don't set content type via Accept header.
|
||||
override def format(implicit request: HttpServletRequest) = ""
|
||||
// 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) {
|
||||
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
|
||||
@@ -36,35 +42,48 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
|
||||
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(context + "/signin?" + StringUtil.urlEncode(path))
|
||||
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(context + "/")
|
||||
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 = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
|
||||
|
||||
private def currentURL: String = defining(request.getQueryString){ queryString =>
|
||||
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
|
||||
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] = session.getAs[Account](Keys.Session.LoginAccount)
|
||||
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){
|
||||
@@ -93,13 +112,19 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
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(html.error("Not Found"))
|
||||
org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
|
||||
}
|
||||
|
||||
protected def Unauthorized()(implicit context: app.Context) =
|
||||
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("/"))
|
||||
@@ -107,27 +132,55 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
if(request.getMethod.toUpperCase == "POST"){
|
||||
org.scalatra.Unauthorized(redirect("/signin"))
|
||||
} else {
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(currentURL)))
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
|
||||
defining(request.getQueryString){ queryString =>
|
||||
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
|
||||
}
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected def baseUrl = defining(request.getRequestURL.toString){ url =>
|
||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||
// 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(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
|
||||
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
|
||||
|
||||
def redirectUrl = if(request.getParameter("redirect") != null){
|
||||
request.getParameter("redirect")
|
||||
} else {
|
||||
currentUrl
|
||||
}
|
||||
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.
|
||||
@@ -149,7 +202,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
|
||||
/**
|
||||
* Base trait for controllers which manages account information.
|
||||
*/
|
||||
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
|
||||
trait AccountManagementControllerBase extends ControllerBase {
|
||||
self: AccountService =>
|
||||
|
||||
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
|
||||
@@ -160,9 +213,9 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
|
||||
}
|
||||
} else {
|
||||
fileId.map { fileId =>
|
||||
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
|
||||
val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
|
||||
FileUtils.moveFile(
|
||||
getTemporaryFile(fileId),
|
||||
new java.io.File(getTemporaryDir(session.getId), fileId),
|
||||
new java.io.File(getUserUploadDir(userName), filename)
|
||||
)
|
||||
updateAvatarImage(userName, Some(filename))
|
||||
@@ -182,28 +235,3 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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] =
|
||||
session.getAndRemove[String](Keys.Session.Upload(fileId))
|
||||
|
||||
}
|
||||
@@ -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(false).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."))
|
||||
}
|
||||
}
|
||||
@@ -1,368 +1,469 @@
|
||||
package app
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import IssuesService._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
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 AccountService 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 =>
|
||||
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
|
||||
getIssue(owner, name, issueId) map {
|
||||
issues.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
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
issues.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)
|
||||
|
||||
// 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) =>
|
||||
defining(repository.owner, repository.name){ case (owner, 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) =>
|
||||
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" => 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 =>
|
||||
defining(params("id").toInt){ issueId =>
|
||||
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 =>
|
||||
defining(params("id").toInt){ issueId =>
|
||||
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 =>
|
||||
defining(params.get("value")){ action =>
|
||||
executeBatch(repository) {
|
||||
handleComment(_, None, repository)( _ => action)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
||||
|
||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
|
||||
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
|
||||
params("checked").split(',') map(_.toInt) foreach execute
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
}
|
||||
|
||||
/**
|
||||
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||
*/
|
||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
||||
(getAction: model.Issue => Option[String] =
|
||||
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
||||
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
val userName = context.loginAccount.get.userName
|
||||
|
||||
getIssue(owner, name, issueId.toString) map { issue =>
|
||||
val (action, recordActivity) =
|
||||
getAction(issue)
|
||||
.collect {
|
||||
case "close" => true -> (Some("close") ->
|
||||
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||
case "reopen" => false -> (Some("reopen") ->
|
||||
Some(recordReopenIssueActivity _))
|
||||
}
|
||||
.map { case (closed, t) =>
|
||||
updateClosed(owner, name, issueId, closed)
|
||||
t
|
||||
}
|
||||
.getOrElse(None -> None)
|
||||
|
||||
val commentId = content
|
||||
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
|
||||
.getOrElse ( action.get.capitalize -> action.get )
|
||||
match {
|
||||
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
||||
}
|
||||
|
||||
// record activity
|
||||
content foreach {
|
||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||
(owner, name, userName, issueId, _)
|
||||
}
|
||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
||||
|
||||
// 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) = {
|
||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||
val filterUser = Map(filter -> params.getOrElse("userName", ""))
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val sessionKey = Keys.Session.Issues(owner, repoName)
|
||||
|
||||
// retrieve search condition
|
||||
val condition = session.putAndGet(sessionKey,
|
||||
if(request.hasQueryString) IssueSearchCondition(request)
|
||||
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
issues.html.list(
|
||||
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
|
||||
countIssue(condition, Map.empty, false, owner -> repoName),
|
||||
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
|
||||
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
|
||||
countIssueGroupByLabels(owner, repoName, condition, filterUser),
|
||||
condition,
|
||||
filter,
|
||||
repository,
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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, issue, 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, issue, _){
|
||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
|
||||
}
|
||||
}
|
||||
action foreach {
|
||||
f.toNotify(repository, issue, _){
|
||||
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,11 +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 service._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||
import util.Implicits._
|
||||
|
||||
class MilestonesController extends MilestonesControllerBase
|
||||
with MilestonesService with RepositoryService with AccountService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
@@ -23,7 +23,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
)(MilestoneForm.apply)
|
||||
|
||||
get("/:owner/:repository/issues/milestones")(referrersOnly { repository =>
|
||||
issues.milestones.html.list(
|
||||
html.list(
|
||||
params.getOrElse("state", "open"),
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name),
|
||||
repository,
|
||||
@@ -31,7 +31,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
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) =>
|
||||
@@ -41,7 +41,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
|
||||
params("milestoneId").toIntOpt.map{ milestoneId =>
|
||||
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
|
||||
html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
package app
|
||||
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 util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import service._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import scala.collection.JavaConverters._
|
||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||
import service.IssuesService._
|
||||
import service.PullRequestService._
|
||||
import util.JGitUtil.DiffInfo
|
||||
import service.RepositoryService.RepositoryTreeNode
|
||||
import util.JGitUtil.CommitInfo
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.eclipse.jgit.merge.MergeStrategy
|
||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import service.WebHookService.WebHookPayload
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with WebHookService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
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 ActivityService with PullRequestService with WebHookService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
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])
|
||||
|
||||
@@ -60,11 +67,30 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
case class MergeForm(message: String)
|
||||
|
||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||
searchPullRequests(None, 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)
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
|
||||
searchPullRequests(Some(params("userName")), 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 =>
|
||||
@@ -75,12 +101,14 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||
val (commits, diffs) =
|
||||
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
||||
|
||||
pulls.html.pullreq(
|
||||
html.pullreq(
|
||||
issue, pullreq,
|
||||
getComments(owner, name, issueId),
|
||||
(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),
|
||||
@@ -90,24 +118,89 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
} 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.openedUserName), Set())
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
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) =>
|
||||
pulls.html.mergeguide(
|
||||
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
|
||||
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,
|
||||
s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
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}/merge"){
|
||||
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.
|
||||
@@ -119,66 +212,32 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// merge
|
||||
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
val conflicted = try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
if (conflicted) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
// creates merge commit
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(merger.getResultTreeId)
|
||||
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
||||
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
mergeCommit.setAuthor(personIdent)
|
||||
mergeCommit.setCommitter(personIdent)
|
||||
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n\n" +
|
||||
form.message)
|
||||
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = git.getRepository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
|
||||
// update refs
|
||||
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(false)
|
||||
refUpdate.setRefLogIdent(personIdent)
|
||||
refUpdate.setRefLogMessage("merged", true)
|
||||
refUpdate.update()
|
||||
// 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)
|
||||
|
||||
commits.flatten.foreach { commit =>
|
||||
if(!existsCommitId(owner, name, commit.id)){
|
||||
insertCommitId(owner, name, commit.id)
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
|
||||
if(pullreq.branch == defaultBranch){
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
}
|
||||
issue.content match {
|
||||
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
|
||||
case _ =>
|
||||
}
|
||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||
}
|
||||
|
||||
// call web hook
|
||||
getWebHookURLs(owner, name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(ownerAccount <- getAccountByUserName(owner)){
|
||||
callWebHook(owner, name, webHookURLs,
|
||||
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
Notifier().toNotify(repository, issue, "merge"){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
@@ -189,26 +248,27 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
val headBranch:Option[String] = params.get("head")
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
|
||||
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
|
||||
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
|
||||
val oldBranch = originRepository.branchList.find( _ == newBranch).getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
|
||||
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
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"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}")
|
||||
} getOrElse {
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,8 +277,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner){
|
||||
@@ -228,35 +288,39 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
|
||||
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 (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)
|
||||
|
||||
val forkedId = getForkedCommitId(oldGit, newGit,
|
||||
originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||
|
||||
val oldId = oldGit.getRepository.resolve(forkedId)
|
||||
val newId = newGit.getRepository.resolve(forkedBranch)
|
||||
(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)
|
||||
|
||||
pulls.html.compare(
|
||||
html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
},
|
||||
originBranch,
|
||||
forkedBranch,
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
forkedRepository,
|
||||
@@ -280,7 +344,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
@@ -288,10 +352,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
pulls.html.mergecheck(
|
||||
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}"){
|
||||
checkConflict(originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch))
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||
}
|
||||
html.mergecheck(conflict)
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
@@ -321,80 +386,24 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
commitIdTo = form.commitIdTo)
|
||||
|
||||
// fetch requested branch
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
|
||||
.call
|
||||
}
|
||||
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"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
getIssue(repository.owner, repository.name, issueId.toString) foreach { issue =>
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||
*/
|
||||
private def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
|
||||
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${branch}"
|
||||
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||
try {
|
||||
// fetch objects from origin repository branch
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||
.setRefSpecs(refSpec)
|
||||
.call
|
||||
|
||||
// merge conflict check
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
|
||||
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
} finally {
|
||||
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
|
||||
*/
|
||||
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||
issueId: Int): Boolean = {
|
||||
LockUtil.lock(s"${userName}/${repositoryName}/merge") {
|
||||
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
// merge
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
*
|
||||
@@ -409,24 +418,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
(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]) = {
|
||||
|
||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
|
||||
using(
|
||||
Git.open(getRepositoryDir(userName, repositoryName)),
|
||||
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
|
||||
@@ -437,18 +430,16 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
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)
|
||||
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
}
|
||||
|
||||
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
|
||||
|
||||
(commits, diffs)
|
||||
}
|
||||
}
|
||||
|
||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
|
||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val sessionKey = Keys.Session.Pulls(owner, repoName)
|
||||
|
||||
@@ -458,17 +449,17 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
pulls.html.list(
|
||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
|
||||
userName,
|
||||
gitbucket.core.issues.html.list(
|
||||
"pulls",
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
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),
|
||||
(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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,267 +1,277 @@
|
||||
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
|
||||
import org.scalatra.i18n.Messages
|
||||
import service.WebHookService.WebHookPayload
|
||||
import util.JGitUtil.CommitInfo
|
||||
import util.ControlUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
with RepositoryService with AccountService with WebHookService
|
||||
with OwnerAuthenticator with UsersAuthenticator
|
||||
|
||||
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
self: RepositoryService with AccountService with WebHookService
|
||||
with OwnerAuthenticator with UsersAuthenticator =>
|
||||
|
||||
// for repository options
|
||||
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||
|
||||
val optionsForm = mapping(
|
||||
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean()))
|
||||
)(OptionsForm.apply)
|
||||
|
||||
// for collaborator addition
|
||||
case class CollaboratorForm(userName: String)
|
||||
|
||||
val collaboratorForm = mapping(
|
||||
"userName" -> trim(label("Username", text(required, collaborator)))
|
||||
)(CollaboratorForm.apply)
|
||||
|
||||
// for web hook url addition
|
||||
case class WebHookForm(url: String)
|
||||
|
||||
val webHookForm = mapping(
|
||||
"url" -> trim(label("url", text(required, webHook)))
|
||||
)(WebHookForm.apply)
|
||||
|
||||
// for transfer ownership
|
||||
case class TransferOwnerShipForm(newOwner: String)
|
||||
|
||||
val transferForm = mapping(
|
||||
"newOwner" -> trim(label("New owner", text(required, transferUser)))
|
||||
)(TransferOwnerShipForm.apply)
|
||||
|
||||
/**
|
||||
* Redirect to the Options page.
|
||||
*/
|
||||
get("/:owner/:repository/settings")(ownerOnly { repository =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the Options page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/options")(ownerOnly {
|
||||
settings.html.options(_, flash.get("info"))
|
||||
})
|
||||
|
||||
/**
|
||||
* Save the repository options.
|
||||
*/
|
||||
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
||||
saveRepositoryOptions(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.description,
|
||||
form.defaultBranch,
|
||||
repository.repository.parentUserName.map { _ =>
|
||||
repository.repository.isPrivate
|
||||
} getOrElse form.isPrivate
|
||||
)
|
||||
// Change repository name
|
||||
if(repository.name != form.repositoryName){
|
||||
// Update database
|
||||
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
|
||||
// Move git repository
|
||||
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
|
||||
}
|
||||
// Move wiki repository
|
||||
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
|
||||
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
|
||||
}
|
||||
}
|
||||
flash += "info" -> "Repository settings has been updated."
|
||||
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the Collaborators page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
|
||||
settings.html.collaborators(
|
||||
getCollaborators(repository.owner, repository.name),
|
||||
getAccountByUserName(repository.owner).get.isGroupAccount,
|
||||
repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* Add the collaborator.
|
||||
*/
|
||||
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
|
||||
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||
addCollaborator(repository.owner, repository.name, form.userName)
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||
})
|
||||
|
||||
/**
|
||||
* Add the collaborator.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
|
||||
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||
removeCollaborator(repository.owner, repository.name, params("name"))
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the web hook page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
|
||||
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
|
||||
})
|
||||
|
||||
/**
|
||||
* Add the web hook URL.
|
||||
*/
|
||||
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
|
||||
addWebHookURL(repository.owner, repository.name, form.url)
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
/**
|
||||
* Delete the web hook URL.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
|
||||
deleteWebHookURL(repository.owner, repository.name, params("url"))
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
/**
|
||||
* Send the test request to registered web hook URLs.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
import scala.collection.JavaConverters._
|
||||
val commits = git.log
|
||||
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||
.setMaxCount(3)
|
||||
.call.iterator.asScala.map(new CommitInfo(_))
|
||||
|
||||
getWebHookURLs(repository.owner, repository.name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(ownerAccount <- getAccountByUserName(repository.owner)){
|
||||
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
|
||||
flash += "info" -> "Test payload deployed!"
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the danger zone.
|
||||
*/
|
||||
get("/:owner/:repository/settings/danger")(ownerOnly {
|
||||
settings.html.danger(_)
|
||||
})
|
||||
|
||||
/**
|
||||
* Transfer repository ownership.
|
||||
*/
|
||||
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
|
||||
// Change repository owner
|
||||
if(repository.owner != form.newOwner){
|
||||
// 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 =>
|
||||
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." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = if(repository.commitCount == 0) List.empty else 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,644 @@
|
||||
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
|
||||
JGitUtil.getContentFromId(git, objectId, true).map {bytes =>
|
||||
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||
} getOrElse NotFound
|
||||
} 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),
|
||||
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
||||
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
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import util._
|
||||
import gitbucket.core.search.html
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
|
||||
import ControlUtil._
|
||||
import service._
|
||||
import Implicits._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SearchController extends SearchControllerBase
|
||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
||||
with RepositorySearchService with IssuesService
|
||||
with ReferrerAuthenticator
|
||||
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
|
||||
|
||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||
with SystemSettingsService with ActivityService with RepositorySearchService
|
||||
with ReferrerAuthenticator =>
|
||||
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
|
||||
|
||||
val searchForm = mapping(
|
||||
"query" -> trim(text(required)),
|
||||
@@ -36,12 +35,12 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||
}
|
||||
|
||||
target.toLowerCase match {
|
||||
case "issue" => search.html.issues(
|
||||
case "issue" => html.issues(
|
||||
searchIssues(repository.owner, repository.name, query),
|
||||
countFiles(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
|
||||
case _ => search.html.code(
|
||||
case _ => html.code(
|
||||
searchFiles(repository.owner, repository.name, query),
|
||||
countIssues(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
@@ -1,21 +1,29 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service.{AccountService, SystemSettingsService}
|
||||
import gitbucket.core.admin.html
|
||||
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
||||
import gitbucket.core.util.AdminAuthenticator
|
||||
import gitbucket.core.ssh.SshServer
|
||||
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
|
||||
with AccountService with AdminAuthenticator
|
||||
|
||||
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
self: SystemSettingsService 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())),
|
||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||
"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()))),
|
||||
@@ -33,20 +41,44 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
"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", text(required))),
|
||||
"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)
|
||||
)(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 {
|
||||
admin.html.system(loadSystemSettings(), flash.get("info"))
|
||||
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")
|
||||
})
|
||||
@@ -1,12 +1,15 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service._
|
||||
import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import util.ControlUtil._
|
||||
import gitbucket.core.service.{RepositoryService, AccountService}
|
||||
import gitbucket.core.admin.users.html
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.Directory._
|
||||
|
||||
class UserManagementController extends UserManagementControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
@@ -23,10 +26,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String])
|
||||
members: String)
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
@@ -47,37 +50,37 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
||||
)(EditUserForm.apply)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" ,optional(text())))
|
||||
"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()))),
|
||||
"memberNames" -> trim(label("Member Names" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||
}.toMap
|
||||
admin.users.html.list(users, members, includeRemoved)
|
||||
|
||||
html.list(users, members, includeRemoved)
|
||||
})
|
||||
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
admin.users.html.user(None)
|
||||
html.user(None)
|
||||
})
|
||||
|
||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||
@@ -88,7 +91,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
admin.users.html.user(getAccountByUserName(userName, true))
|
||||
html.user(getAccountByUserName(userName, true))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
@@ -122,24 +125,32 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
get("/admin/users/_newgroup")(adminOnly {
|
||||
admin.users.html.group(None, Nil)
|
||||
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))
|
||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
}
|
||||
}.toList)
|
||||
updateImage(form.groupName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
}
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
|
||||
defining(params("groupName"), form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
}
|
||||
}.toList){ case (groupName, members) =>
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
updateGroup(groupName, form.url, form.isRemoved)
|
||||
|
||||
@@ -155,11 +166,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
} else {
|
||||
// Update GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
updateGroupMembers(form.groupName, members)
|
||||
// Update COLLABORATOR for group repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
members.foreach { case (userName, isManager) =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
@@ -172,8 +183,22 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
post("/admin/users/_usercheck")(adminOnly {
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
})
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
params.get(paramName).flatMap { userName =>
|
||||
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
||||
Some("You can't disable your account yourself")
|
||||
else
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service._
|
||||
import util._
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import gitbucket.core.wiki.html
|
||||
import gitbucket.core.service.{RepositoryService, WikiService, ActivityService, AccountService}
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.FlashMapSupport
|
||||
import org.scalatra.i18n.Messages
|
||||
import scala.Some
|
||||
import java.util.ResourceBundle
|
||||
|
||||
class WikiController extends WikiControllerBase
|
||||
with WikiService with RepositoryService with AccountService with ActivityService
|
||||
with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||
|
||||
trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
self: WikiService with RepositoryService 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, id: String)
|
||||
|
||||
@@ -39,7 +37,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
|
||||
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))
|
||||
html.page("Home", page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
|
||||
})
|
||||
|
||||
@@ -47,7 +46,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
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))
|
||||
html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
|
||||
})
|
||||
|
||||
@@ -56,7 +56,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
|
||||
using(Git.open(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 Right((logs, hasNext)) => html.history(Some(pageName), logs, repository)
|
||||
case Left(_) => NotFound
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
|
||||
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -76,7 +76,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -106,13 +106,21 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
|
||||
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)
|
||||
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
||||
defining(context.loginAccount.get){ loginAccount =>
|
||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||
form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
|
||||
saveWikiPage(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.currentPageName,
|
||||
form.pageName,
|
||||
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
|
||||
loginAccount,
|
||||
form.message.getOrElse(""),
|
||||
Some(form.id)
|
||||
).map { commitId =>
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
||||
}
|
||||
@@ -121,7 +129,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
|
||||
wiki.html.edit("", None, _)
|
||||
html.edit("", None, _)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||
@@ -148,14 +156,14 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
||||
wiki.html.pages(getWikiPageList(repository.owner, repository.name), repository,
|
||||
html.pages(getWikiPageList(repository.owner, repository.name), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
JGitUtil.getCommitLog(git, "master") match {
|
||||
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
|
||||
case Right((logs, hasNext)) => html.history(None, logs, repository)
|
||||
case Left(_) => NotFound
|
||||
}
|
||||
}
|
||||
@@ -165,8 +173,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
|
||||
val path = multiParams("splat").head
|
||||
|
||||
getFileContent(repository.owner, repository.name, path).map { bytes =>
|
||||
contentType = FileUtil.getContentType(path, bytes)
|
||||
bytes
|
||||
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
21
src/main/scala/gitbucket/core/model/AccessToken.scala
Normal file
21
src/main/scala/gitbucket/core/model/AccessToken.scala
Normal file
@@ -0,0 +1,21 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
|
||||
trait AccessTokenComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
lazy val AccessTokens = TableQuery[AccessTokens]
|
||||
|
||||
class AccessTokens(tag: Tag) extends Table[AccessToken](tag, "ACCESS_TOKEN") {
|
||||
val accessTokenId = column[Int]("ACCESS_TOKEN_ID", O AutoInc)
|
||||
val userName = column[String]("USER_NAME")
|
||||
val tokenHash = column[String]("TOKEN_HASH")
|
||||
val note = column[String]("NOTE")
|
||||
def * = (accessTokenId, userName, tokenHash, note) <> (AccessToken.tupled, AccessToken.unapply)
|
||||
}
|
||||
}
|
||||
case class AccessToken(
|
||||
accessTokenId: Int = 0,
|
||||
userName: String,
|
||||
tokenHash: String,
|
||||
note: String
|
||||
)
|
||||
39
src/main/scala/gitbucket/core/model/Account.scala
Normal file
39
src/main/scala/gitbucket/core/model/Account.scala
Normal file
@@ -0,0 +1,39 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait AccountComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
import self._
|
||||
|
||||
lazy val Accounts = TableQuery[Accounts]
|
||||
|
||||
class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") {
|
||||
val userName = column[String]("USER_NAME", O PrimaryKey)
|
||||
val fullName = column[String]("FULL_NAME")
|
||||
val mailAddress = column[String]("MAIL_ADDRESS")
|
||||
val password = column[String]("PASSWORD")
|
||||
val isAdmin = column[Boolean]("ADMINISTRATOR")
|
||||
val url = column[String]("URL")
|
||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
||||
val image = column[String]("IMAGE")
|
||||
val groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||
val removed = column[Boolean]("REMOVED")
|
||||
def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply)
|
||||
}
|
||||
}
|
||||
|
||||
case class Account(
|
||||
userName: String,
|
||||
fullName: String,
|
||||
mailAddress: String,
|
||||
password: String,
|
||||
isAdmin: Boolean,
|
||||
url: Option[String],
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date,
|
||||
lastLoginDate: Option[java.util.Date],
|
||||
image: Option[String],
|
||||
isGroupAccount: Boolean,
|
||||
isRemoved: Boolean
|
||||
)
|
||||
29
src/main/scala/gitbucket/core/model/Activity.scala
Normal file
29
src/main/scala/gitbucket/core/model/Activity.scala
Normal file
@@ -0,0 +1,29 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait ActivityComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
import self._
|
||||
|
||||
lazy val Activities = TableQuery[Activities]
|
||||
|
||||
class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
|
||||
val activityId = column[Int]("ACTIVITY_ID", O AutoInc)
|
||||
val activityUserName = column[String]("ACTIVITY_USER_NAME")
|
||||
val activityType = column[String]("ACTIVITY_TYPE")
|
||||
val message = column[String]("MESSAGE")
|
||||
val additionalInfo = column[String]("ADDITIONAL_INFO")
|
||||
val activityDate = column[java.util.Date]("ACTIVITY_DATE")
|
||||
def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
|
||||
}
|
||||
}
|
||||
|
||||
case class Activity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
activityType: String,
|
||||
message: String,
|
||||
additionalInfo: Option[String],
|
||||
activityDate: java.util.Date,
|
||||
activityId: Int = 0
|
||||
)
|
||||
57
src/main/scala/gitbucket/core/model/BasicTemplate.scala
Normal file
57
src/main/scala/gitbucket/core/model/BasicTemplate.scala
Normal file
@@ -0,0 +1,57 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
protected[model] trait TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
|
||||
trait BasicTemplate { self: Table[_] =>
|
||||
val userName = column[String]("USER_NAME")
|
||||
val repositoryName = column[String]("REPOSITORY_NAME")
|
||||
|
||||
def byRepository(owner: String, repository: String) =
|
||||
(userName === owner.bind) && (repositoryName === repository.bind)
|
||||
|
||||
def byRepository(userName: Column[String], repositoryName: Column[String]) =
|
||||
(this.userName === userName) && (this.repositoryName === repositoryName)
|
||||
}
|
||||
|
||||
trait IssueTemplate extends BasicTemplate { self: Table[_] =>
|
||||
val issueId = column[Int]("ISSUE_ID")
|
||||
|
||||
def byIssue(owner: String, repository: String, issueId: Int) =
|
||||
byRepository(owner, repository) && (this.issueId === issueId.bind)
|
||||
|
||||
def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
|
||||
byRepository(userName, repositoryName) && (this.issueId === issueId)
|
||||
}
|
||||
|
||||
trait LabelTemplate extends BasicTemplate { self: Table[_] =>
|
||||
val labelId = column[Int]("LABEL_ID")
|
||||
|
||||
def byLabel(owner: String, repository: String, labelId: Int) =
|
||||
byRepository(owner, repository) && (this.labelId === labelId.bind)
|
||||
|
||||
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
|
||||
byRepository(userName, repositoryName) && (this.labelId === labelId)
|
||||
}
|
||||
|
||||
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
|
||||
val milestoneId = column[Int]("MILESTONE_ID")
|
||||
|
||||
def byMilestone(owner: String, repository: String, milestoneId: Int) =
|
||||
byRepository(owner, repository) && (this.milestoneId === milestoneId.bind)
|
||||
|
||||
def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
|
||||
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
|
||||
}
|
||||
|
||||
trait CommitTemplate extends BasicTemplate { self: Table[_] =>
|
||||
val commitId = column[String]("COMMIT_ID")
|
||||
|
||||
def byCommit(owner: String, repository: String, commitId: String) =
|
||||
byRepository(owner, repository) && (this.commitId === commitId)
|
||||
|
||||
def byCommit(owner: Column[String], repository: Column[String], commitId: Column[String]) =
|
||||
byRepository(userName, repositoryName) && (this.commitId === commitId)
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user