mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 12:16:32 +02:00
Compare commits
663 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
977f856854 | ||
|
|
da2a7bf77d | ||
|
|
3da3a048f0 | ||
|
|
7b5b453e56 | ||
|
|
c18f95edf8 | ||
|
|
71cf043f56 | ||
|
|
a31e4b5897 | ||
|
|
1679da4abe | ||
|
|
505bc71f9a | ||
|
|
4bc057c653 | ||
|
|
8eee13d7aa | ||
|
|
8981e339b4 | ||
|
|
e1dd5dd057 | ||
|
|
cb64f8eab8 | ||
|
|
c47d50d0df | ||
|
|
1f46da2273 | ||
|
|
06fc26cd06 | ||
|
|
3a4f9b9027 | ||
|
|
f98c849c7c | ||
|
|
aa0bd5b34a | ||
|
|
b52e904ed1 | ||
|
|
70e0dcf99d | ||
|
|
56bb20dfd2 | ||
|
|
7d7d2f488d | ||
|
|
72affd67b9 | ||
|
|
0cf1f43deb | ||
|
|
8494c682a7 | ||
|
|
1af5611159 | ||
|
|
4d39f63ef7 | ||
|
|
120d1c2fff | ||
|
|
62e9c0358a | ||
|
|
5a90848c75 | ||
|
|
760d443f74 | ||
|
|
5ee0e75dfe | ||
|
|
3b4d2d6f91 | ||
|
|
dfaabeb41d | ||
|
|
0fae2dac35 | ||
|
|
4db4fe28b4 | ||
|
|
5b87efa032 | ||
|
|
3ad609bad7 | ||
|
|
8145cba111 | ||
|
|
24b9a9a12c | ||
|
|
ee7220ebd2 | ||
|
|
8fb72fd55e | ||
|
|
a1eded2d9a | ||
|
|
7f184e1126 | ||
|
|
09aafbcce1 | ||
|
|
7f5024a746 | ||
|
|
8fec0870a8 | ||
|
|
a8d2afaff7 | ||
|
|
8fd92f1c2f | ||
|
|
419ea16ead | ||
|
|
e72d808a3c | ||
|
|
44e8c0a9be | ||
|
|
e2c39d7815 | ||
|
|
687cd54f9a | ||
|
|
911754e1dc | ||
|
|
0067cbce6f | ||
|
|
f40f8427aa | ||
|
|
98ceff2391 | ||
|
|
642a51a208 | ||
|
|
9ec7c321d8 | ||
|
|
a3c419b6f5 | ||
|
|
15c28cffa4 | ||
|
|
f4d0f16481 | ||
|
|
45535e4fdf | ||
|
|
64635c5dc6 | ||
|
|
2fd95c7f1a | ||
|
|
eb6da85183 | ||
|
|
bcc05f021c | ||
|
|
d58ed55c3a | ||
|
|
057f029c80 | ||
|
|
c9a12ff913 | ||
|
|
66bf00b5d3 | ||
|
|
7ba3ca6f15 | ||
|
|
a1bacccc09 | ||
|
|
333eeb4bad | ||
|
|
3f2935612d | ||
|
|
4c87bdd959 | ||
|
|
3543073150 | ||
|
|
e50fe604c2 | ||
|
|
63369258bd | ||
|
|
e7c3376303 | ||
|
|
86163f66ce | ||
|
|
518f0bfc28 | ||
|
|
0a759f6127 | ||
|
|
afa79d01b1 | ||
|
|
860fc8ef4c | ||
|
|
5a2224623b | ||
|
|
dda3458e80 | ||
|
|
df06f509f5 | ||
|
|
0b4d0be1b4 | ||
|
|
0bef8d29d5 | ||
|
|
6265faa14f | ||
|
|
7593c97ad3 | ||
|
|
7c8de834bc | ||
|
|
637e9f906b | ||
|
|
97035be2e5 | ||
|
|
80fedbd335 | ||
|
|
3f6ebc1164 | ||
|
|
27b99319d3 | ||
|
|
a46d1ecf69 | ||
|
|
fb531526bd | ||
|
|
21d6143e40 | ||
|
|
129b424a3a | ||
|
|
689334a599 | ||
|
|
b3ee2222f3 | ||
|
|
da5e4fe5b1 | ||
|
|
f3b7318453 | ||
|
|
9874eb7243 | ||
|
|
cc6f4d70da | ||
|
|
9b06bfaaf5 | ||
|
|
cdf0d06bcc | ||
|
|
501f542982 | ||
|
|
1a3504e885 | ||
|
|
fa617badc1 | ||
|
|
1a1267dc60 | ||
|
|
361babd327 | ||
|
|
64cacb18a4 | ||
|
|
833cfc3465 | ||
|
|
95746de5aa | ||
|
|
4f3c780d05 | ||
|
|
33a079e55f | ||
|
|
27eb1c36bd | ||
|
|
5c12cca7f5 | ||
|
|
206d597d9b | ||
|
|
1b4c621fef | ||
|
|
03d4b9e9c6 | ||
|
|
82a9d9f7cf | ||
|
|
3e79dcf7a4 | ||
|
|
c9f3ec12a7 | ||
|
|
b781696803 | ||
|
|
8dcb8c2ecb | ||
|
|
b599219852 | ||
|
|
58078989f8 | ||
|
|
4276c0970b | ||
|
|
4abd4a4bac | ||
|
|
2b2ae718f7 | ||
|
|
ef201e3319 | ||
|
|
0ab28e6daa | ||
|
|
9b3e8bd22b | ||
|
|
15e8527e01 | ||
|
|
f991f3b454 | ||
|
|
d6a39403e2 | ||
|
|
aa065fd030 | ||
|
|
b1b132dfc3 | ||
|
|
90c379b3b9 | ||
|
|
b71ff6f179 | ||
|
|
f63542dbd0 | ||
|
|
63e605d99c | ||
|
|
9e313bb2b3 | ||
|
|
a03fc4cf4a | ||
|
|
d18f92cfbf | ||
|
|
b96821ca44 | ||
|
|
c61ecdef7a | ||
|
|
b50ca63c2b | ||
|
|
f6624c0002 | ||
|
|
d62fc6e135 | ||
|
|
926c4d948f | ||
|
|
5fd87ca764 | ||
|
|
1042c50cc3 | ||
|
|
adff36c44b | ||
|
|
e8f6d2b990 | ||
|
|
a3b3262ad5 | ||
|
|
9efba82e94 | ||
|
|
781d465f8d | ||
|
|
766867f341 | ||
|
|
e237a44f56 | ||
|
|
249430449b | ||
|
|
73d935efe3 | ||
|
|
79251ef1d1 | ||
|
|
1081c0f16c | ||
|
|
3302b607f1 | ||
|
|
f13a3ee580 | ||
|
|
927969ac17 | ||
|
|
2d8aa4f8b5 | ||
|
|
645af4d2c0 | ||
|
|
34240d16b5 | ||
|
|
89210985d4 | ||
|
|
9d6258861a | ||
|
|
6661314be5 | ||
|
|
e187d026cc | ||
|
|
100b34085c | ||
|
|
ba75079409 | ||
|
|
cbb847704b | ||
|
|
739c99edce | ||
|
|
af2e2f5fd1 | ||
|
|
09fcf9c003 | ||
|
|
5dad0777d7 | ||
|
|
be3aa21651 | ||
|
|
abd729e45f | ||
|
|
162e9a9feb | ||
|
|
7500d017d5 | ||
|
|
e8c4004d5a | ||
|
|
a6f4d9d7cf | ||
|
|
3399278925 | ||
|
|
4457a317e6 | ||
|
|
990d082422 | ||
|
|
dfe3fbc02f | ||
|
|
c077dd0046 | ||
|
|
9cc6beead7 | ||
|
|
92f6792ad9 | ||
|
|
25031eadfb | ||
|
|
a7024615e1 | ||
|
|
8191a4bedb | ||
|
|
7e1f5aa353 | ||
|
|
44e161f6ec | ||
|
|
7bd17ed038 | ||
|
|
a47404130b | ||
|
|
669949aa8c | ||
|
|
c773bb90f6 | ||
|
|
18f525523b | ||
|
|
3bfd228df1 | ||
|
|
c072ab2dc5 | ||
|
|
53e06c5e86 | ||
|
|
b0a749bdc1 | ||
|
|
809443a46f | ||
|
|
59ef44dd71 | ||
|
|
d77973a29e | ||
|
|
be6bc16d3d | ||
|
|
407e926abe | ||
|
|
117655b011 | ||
|
|
2ea4a5a7ef | ||
|
|
30b0279efe | ||
|
|
7511701bb2 | ||
|
|
a919ae3430 | ||
|
|
c30ee24f8d | ||
|
|
10b5de571e | ||
|
|
c6f4ec7250 | ||
|
|
b229058726 | ||
|
|
21363794b6 | ||
|
|
1754d37ac0 | ||
|
|
1c36b185ec | ||
|
|
1a4b94e5df | ||
|
|
31bd8e1741 | ||
|
|
e8824ad1ac | ||
|
|
17636a5ecf | ||
|
|
8f36ff60de | ||
|
|
7ad605afba | ||
|
|
f080e8693e | ||
|
|
e27bf7868f | ||
|
|
db55fe9ff3 | ||
|
|
de4db0764c | ||
|
|
9d644b4625 | ||
|
|
354dc27e84 | ||
|
|
1c639474d3 | ||
|
|
4d8b47732e | ||
|
|
8650abf679 | ||
|
|
27dcc2ef48 | ||
|
|
94a12cd28c | ||
|
|
a982b64fd4 | ||
|
|
ee5b4bbf2e | ||
|
|
d09ddd8d07 | ||
|
|
c7bf47820c | ||
|
|
4dfb01d59e | ||
|
|
54bef6505f | ||
|
|
2aa71ed217 | ||
|
|
ef3b02b718 | ||
|
|
d90938421f | ||
|
|
e4992bbd17 | ||
|
|
7853b22522 | ||
|
|
9475215fa6 | ||
|
|
8622bbd2ac | ||
|
|
0cdcc252e0 | ||
|
|
9477cca8e8 | ||
|
|
176e427058 | ||
|
|
1bf26cacb9 | ||
|
|
38e8247483 | ||
|
|
44aa12108c | ||
|
|
dd73405aa9 | ||
|
|
6710393a53 | ||
|
|
711aee1c8b | ||
|
|
51be1048d5 | ||
|
|
50166f04d8 | ||
|
|
7eb6fea08b | ||
|
|
8e3c054da4 | ||
|
|
5249b67df1 | ||
|
|
583830c1c2 | ||
|
|
bb75f1c034 | ||
|
|
b7a9236283 | ||
|
|
7e0e172d37 | ||
|
|
ea37a34476 | ||
|
|
fb5564de07 | ||
|
|
cead7e9e4e | ||
|
|
7cf23b6c56 | ||
|
|
1080821862 | ||
|
|
0bf843ce2a | ||
|
|
b8bd8f5512 | ||
|
|
12fba30ef8 | ||
|
|
dc5cb74f4d | ||
|
|
446a524b5a | ||
|
|
121110aede | ||
|
|
c76a8562ea | ||
|
|
495d8069f5 | ||
|
|
70af6d0c35 | ||
|
|
9777d543b1 | ||
|
|
abfc6eea1e | ||
|
|
fd2f3e252c | ||
|
|
3b6d0065a2 | ||
|
|
51acf72e0d | ||
|
|
6ad724d80c | ||
|
|
7ffe2ab864 | ||
|
|
2f6febb748 | ||
|
|
092c897150 | ||
|
|
30f072f925 | ||
|
|
72d0282613 | ||
|
|
a56f766497 | ||
|
|
31b5583896 | ||
|
|
7c95bfc77e | ||
|
|
c6804fe589 | ||
|
|
fc41e282ce | ||
|
|
d3b21a4e39 | ||
|
|
7b6d9850a2 | ||
|
|
84d97817e3 | ||
|
|
2c6d2176bb | ||
|
|
7840d28ed2 | ||
|
|
d4fd2e6cd6 | ||
|
|
70e8385246 | ||
|
|
3fd23f1749 | ||
|
|
a3e3aea4e0 | ||
|
|
dcfb8ad8eb | ||
|
|
35f82e91bc | ||
|
|
f1533cb168 | ||
|
|
763fbec0ca | ||
|
|
111c893d94 | ||
|
|
80c38a45c5 | ||
|
|
548860607b | ||
|
|
66b5fe7337 | ||
|
|
2b2a117912 | ||
|
|
39a10fd167 | ||
|
|
93f8dbd42a | ||
|
|
4f280d0df8 | ||
|
|
5e9ff3278a | ||
|
|
b4b68f0e17 | ||
|
|
97bd324cb0 | ||
|
|
2738406710 | ||
|
|
8e78345cbe | ||
|
|
bd9e064137 | ||
|
|
eb49365bcb | ||
|
|
de92e28c7a | ||
|
|
c18f8fd87b | ||
|
|
34272cb4c4 | ||
|
|
b3f8a02494 | ||
|
|
f767a55350 | ||
|
|
9b2c3848d9 | ||
|
|
e34d016581 | ||
|
|
b64b447b42 | ||
|
|
c6a4c13394 | ||
|
|
c8822cb4ca | ||
|
|
c05e7218f3 | ||
|
|
01872d3440 | ||
|
|
91bbb3e4dc | ||
|
|
80ebd9fb0e | ||
|
|
2ab217251a | ||
|
|
2b20f6c74c | ||
|
|
042f855cd5 | ||
|
|
720ab7e0a3 | ||
|
|
4b1b100aa5 | ||
|
|
541b7e2a79 | ||
|
|
00cd3adc7b | ||
|
|
5a97a518a6 | ||
|
|
d7219068cd | ||
|
|
a79c07f095 | ||
|
|
9f27f70c87 | ||
|
|
8fa79db368 | ||
|
|
a1efa60741 | ||
|
|
a08a212fdc | ||
|
|
6166eb3743 | ||
|
|
5194fc5f15 | ||
|
|
1a97beb8cf | ||
|
|
99d23398ad | ||
|
|
6accdefb8c | ||
|
|
98fc64deaa | ||
|
|
30a8cefc37 | ||
|
|
9d47c3ccb3 | ||
|
|
5a1ab8d485 | ||
|
|
5d526f243e | ||
|
|
d13bb47ee7 | ||
|
|
8b8c6ee861 | ||
|
|
0a2d95e434 | ||
|
|
fdd119c477 | ||
|
|
4f94ca1384 | ||
|
|
ed21ee8bdb | ||
|
|
efd257dee0 | ||
|
|
bacf391a39 | ||
|
|
e8a1543466 | ||
|
|
81c79003ec | ||
|
|
73cf9661ac | ||
|
|
6e994b0ae1 | ||
|
|
c5da975cea | ||
|
|
1f3ef962e8 | ||
|
|
f6066a0361 | ||
|
|
ace65cf261 | ||
|
|
1df537ce5c | ||
|
|
bf64f6b4f4 | ||
|
|
0283ec574d | ||
|
|
d566f64e8b | ||
|
|
bb9add9da9 | ||
|
|
75d085a2c4 | ||
|
|
4eab07ffaf | ||
|
|
cf7aaa25cd | ||
|
|
8011b22de6 | ||
|
|
a108489d71 | ||
|
|
185c132771 | ||
|
|
3b11e905a1 | ||
|
|
5a04fe7ae6 | ||
|
|
92c73062cc | ||
|
|
d07624bdc1 | ||
|
|
e1dbe80ccd | ||
|
|
6d69a52292 | ||
|
|
68af5479c8 | ||
|
|
3970eca8dc | ||
|
|
b11d36c3a5 | ||
|
|
3c53fd8618 | ||
|
|
4ca4c57fff | ||
|
|
317a6fde30 | ||
|
|
ad08d385e6 | ||
|
|
805e12aceb | ||
|
|
3a45912400 | ||
|
|
19817e2659 | ||
|
|
50dc205ef7 | ||
|
|
2402a3ac72 | ||
|
|
e1c155d09d | ||
|
|
84c3bc4ad4 | ||
|
|
353784c23e | ||
|
|
a359624f01 | ||
|
|
a0f684cfdf | ||
|
|
1ea1e74a0c | ||
|
|
8f7c5fc922 | ||
|
|
667ef680c1 | ||
|
|
972ab0df50 | ||
|
|
1fddc01f6e | ||
|
|
bb2e77d899 | ||
|
|
a3daf13c15 | ||
|
|
fb2b2e37ce | ||
|
|
c1381179aa | ||
|
|
9e2dc3f892 | ||
|
|
5aa548d613 | ||
|
|
5225a95d3a | ||
|
|
53b7a1fce5 | ||
|
|
02369a4949 | ||
|
|
1ca548991b | ||
|
|
0772070523 | ||
|
|
4bf3848856 | ||
|
|
512425de4c | ||
|
|
7f28bd6a26 | ||
|
|
4088b2c1e8 | ||
|
|
919d55c002 | ||
|
|
068bbd0c3b | ||
|
|
9f50528192 | ||
|
|
4c149cf01c | ||
|
|
c86c706406 | ||
|
|
3b0a0f55b5 | ||
|
|
4232b8184e | ||
|
|
e5f3dfe293 | ||
|
|
22af94d36a | ||
|
|
d6b6781861 | ||
|
|
2222299793 | ||
|
|
fdd9a184b5 | ||
|
|
99492e3f8e | ||
|
|
a42c40bbc1 | ||
|
|
2794f9fcfc | ||
|
|
28c0262e74 | ||
|
|
8634191bd2 | ||
|
|
f73c86d533 | ||
|
|
f042d709ac | ||
|
|
e2a6149a93 | ||
|
|
b2a7e2c7e2 | ||
|
|
89fc143075 | ||
|
|
a754a92799 | ||
|
|
dc26fcf609 | ||
|
|
b9db57eeef | ||
|
|
9b377c727d | ||
|
|
e5b8d81bb4 | ||
|
|
c85b31a7d5 | ||
|
|
6580e5458a | ||
|
|
4e4e65eaa6 | ||
|
|
9d19aad384 | ||
|
|
c16a9f234b | ||
|
|
ace551c33d | ||
|
|
1e6e686692 | ||
|
|
afdcc3f7c0 | ||
|
|
00e64bc46c | ||
|
|
a959e1820f | ||
|
|
3dfbdbfe51 | ||
|
|
5c46dc0bd3 | ||
|
|
db60db674f | ||
|
|
687a4f14e1 | ||
|
|
bb10365b8b | ||
|
|
74ed3bf6a0 | ||
|
|
d1d7fdc488 | ||
|
|
67775a4c62 | ||
|
|
317b5cb096 | ||
|
|
2929517d7e | ||
|
|
51e788396d | ||
|
|
1321653bf6 | ||
|
|
3899854854 | ||
|
|
c0ca842ba7 | ||
|
|
24b05d28db | ||
|
|
f0268b105c | ||
|
|
0a46e180a9 | ||
|
|
e6a215a9c3 | ||
|
|
8ca7117065 | ||
|
|
ba0a07b835 | ||
|
|
4a35b65c2c | ||
|
|
836fa47812 | ||
|
|
5b658ef6ff | ||
|
|
e9ff24d9a7 | ||
|
|
a92051a4c3 | ||
|
|
77b3650580 | ||
|
|
67ee6857ad | ||
|
|
5ab15c0a14 | ||
|
|
96a3f2c301 | ||
|
|
85707264c4 | ||
|
|
ed05422ea8 | ||
|
|
8f10c8051e | ||
|
|
41fff399b5 | ||
|
|
9e237647b0 | ||
|
|
1be53c6746 | ||
|
|
f2368b03c0 | ||
|
|
95284c0b36 | ||
|
|
f1e21a93fb | ||
|
|
689811f659 | ||
|
|
33ccb4e98c | ||
|
|
29f6e98f9c | ||
|
|
775c8cc064 | ||
|
|
9a974d047c | ||
|
|
3fd252d2db | ||
|
|
43edb034c8 | ||
|
|
e629ca391e | ||
|
|
0db7eba3f2 | ||
|
|
a472abc88e | ||
|
|
861c619c19 | ||
|
|
124f331963 | ||
|
|
76fcd44191 | ||
|
|
2124c0b88c | ||
|
|
2f5ab8e3b9 | ||
|
|
32c8b6914b | ||
|
|
7e9d940f64 | ||
|
|
59e826b630 | ||
|
|
88f3ee4b13 | ||
|
|
b7380a084e | ||
|
|
cb65e790ae | ||
|
|
573eabee93 | ||
|
|
f0d4c6546a | ||
|
|
b0943c87c8 | ||
|
|
4f1208ea98 | ||
|
|
02f16639ea | ||
|
|
cd5e28c0b8 | ||
|
|
6f7579f8d9 | ||
|
|
f1d2a71b49 | ||
|
|
fd4fe0dc0d | ||
|
|
3d5e4a4225 | ||
|
|
10ffb452d0 | ||
|
|
3218bddbdc | ||
|
|
59f78dcbcb | ||
|
|
0fe062a02f | ||
|
|
869eaf8cfd | ||
|
|
5b445c9736 | ||
|
|
6f3f3eaa99 | ||
|
|
a23ce92676 | ||
|
|
ad55d5199d | ||
|
|
bd05895761 | ||
|
|
c68dd6c891 | ||
|
|
daae9ae1e7 | ||
|
|
33dde75ae1 | ||
|
|
b2e1e1796d | ||
|
|
c4a682773c | ||
|
|
07e1873cb3 | ||
|
|
14d0c4dc32 | ||
|
|
19f79b6e54 | ||
|
|
e0b9fc9481 | ||
|
|
16c28ad938 | ||
|
|
0e5af9ffa9 | ||
|
|
a0f6f03d4e | ||
|
|
f4cdfc5f32 | ||
|
|
b45988705b | ||
|
|
dd50239759 | ||
|
|
7297a07466 | ||
|
|
e5fa43f91c | ||
|
|
88080c8d02 | ||
|
|
d38dfee540 | ||
|
|
4da6a7b6f9 | ||
|
|
957dfeed6d | ||
|
|
aa5a07b98e | ||
|
|
fd1ee07297 | ||
|
|
07026125a9 | ||
|
|
badb747d24 | ||
|
|
e27c20d600 | ||
|
|
eb46c94c7c | ||
|
|
2e58f82a61 | ||
|
|
df28013b18 | ||
|
|
8f4e285974 | ||
|
|
08c4eeeefe | ||
|
|
1fdc2c629c | ||
|
|
a6767a1c3d | ||
|
|
c539d6b643 | ||
|
|
80c3ffdfd7 | ||
|
|
f80f2106fe | ||
|
|
cbfae66882 | ||
|
|
8a5014fcbe | ||
|
|
54d1bff213 | ||
|
|
0075664b9a | ||
|
|
a2c26f0f2c | ||
|
|
45f41a13e4 | ||
|
|
faae237ac5 | ||
|
|
e01758e74c | ||
|
|
db7dd31c79 | ||
|
|
7d7ac5e2be | ||
|
|
577016a33f | ||
|
|
fdf2102923 | ||
|
|
8b47e57be0 | ||
|
|
8dad6b64b0 | ||
|
|
05d36abdab | ||
|
|
04e31c5b4f | ||
|
|
6470428a85 | ||
|
|
a7efb3989a | ||
|
|
32072d0bbf | ||
|
|
78a3e4454d | ||
|
|
1ff68111b4 | ||
|
|
e576178e1e | ||
|
|
0f8bc2b03d | ||
|
|
43565458d4 | ||
|
|
71cc3be6d5 | ||
|
|
9a8eef7b19 | ||
|
|
a08c4368b7 | ||
|
|
3b456b2aab | ||
|
|
31559418ba | ||
|
|
692a6e43bc | ||
|
|
af4cce654c | ||
|
|
c63b02fd4a | ||
|
|
e6974b6e51 | ||
|
|
da3b7dbeff | ||
|
|
9737bd7012 | ||
|
|
55fd8e5e2d | ||
|
|
9c4f181d93 | ||
|
|
374342cfc1 | ||
|
|
6fbdd237d1 | ||
|
|
4e78f01a09 | ||
|
|
8853264808 | ||
|
|
6db9b8038f | ||
|
|
a7b48d63e4 | ||
|
|
3677906e95 | ||
|
|
5568acc5f3 | ||
|
|
c467594199 | ||
|
|
59c18056fc | ||
|
|
d8d18ed25c | ||
|
|
83fd2648f5 | ||
|
|
8e81758941 | ||
|
|
41a6a29771 | ||
|
|
3e0a50926f | ||
|
|
dff816324d | ||
|
|
a33e2c6e36 | ||
|
|
75ef82d18a | ||
|
|
eb3c522122 | ||
|
|
6c8bcfc62e | ||
|
|
e408eb43bb | ||
|
|
dc0aa0851e | ||
|
|
51d7c43489 | ||
|
|
70c386a934 | ||
|
|
08eb21844a | ||
|
|
7b37d6b571 | ||
|
|
f52bd2bcc0 |
7
.github/CONTRIBUTING.md
vendored
Normal file
7
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Guideline for Issues
|
||||
|
||||
- At first, See [FAQ](https://github.com/gitbucket/gitbucket/wiki/FAQ) and check issues whether there is a same question or request in the past.
|
||||
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
|
||||
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||
- Write an issue in English. At least, write subject in English.
|
||||
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
||||
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
### Before submitting an issue to Gitbucket I have first:
|
||||
|
||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
|
||||
- [] searched for similar already existing issue
|
||||
- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
|
||||
|
||||
*(if you have performed all the above, remove the paragraph and continue describing the issue with template below)*
|
||||
|
||||
## Issue
|
||||
**Impacted version**: xxxx
|
||||
|
||||
**Deployment mode**: *explain here how you use gitbucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
|
||||
|
||||
**Problem description**:
|
||||
- *be as explicit has you can*
|
||||
- *describe the problem and its symptoms*
|
||||
- *explain how to reproduce*
|
||||
- *attach whatever information that can help understanding the context (screen capture, log files)*
|
||||
- *do your best to use a correct english (re-read yourself)*
|
||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
### Before submitting a pull-request to Gitbucket I have first:
|
||||
|
||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
|
||||
- [] rebased my branch over master
|
||||
- [] verified that project is compiling
|
||||
- [] verified that tests are passing
|
||||
- [] squashed my commits as appropriate *(keep several commits if it is relevant to understand the PR)*
|
||||
- [] [marked as closed](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
*.class
|
||||
*.log
|
||||
.ensime
|
||||
.ensime_cache
|
||||
|
||||
# sbt specific
|
||||
dist/*
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
language: scala
|
||||
scala:
|
||||
- 2.11.6
|
||||
sudo: false
|
||||
script:
|
||||
- sbt test
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
140
README.md
140
README.md
@@ -1,8 +1,7 @@
|
||||
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://travis-ci.org/takezoe/gitbucket)
|
||||
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://travis-ci.org/gitbucket/gitbucket)
|
||||
=========
|
||||
|
||||
GitBucket is the easily installable GitHub clone powered by Scala.
|
||||
|
||||
GitBucket is a GitHub clone powered by Scala which has easy installation and high extensibility.
|
||||
|
||||
Features
|
||||
--------
|
||||
@@ -10,35 +9,21 @@ The current version of GitBucket provides a basic features below:
|
||||
|
||||
- Public / Private Git repository (http and ssh access)
|
||||
- Repository viewer and online file editing
|
||||
- Repository search (Code and Issues)
|
||||
- Wiki
|
||||
- Issues
|
||||
- Fork / Pull request
|
||||
- Mail notification
|
||||
- Activity timeline
|
||||
- User management (for Administrators)
|
||||
- Group (like Organization in Github)
|
||||
- LDAP integration
|
||||
- Gravatar support
|
||||
- Issues / Pull request
|
||||
- Email notification
|
||||
- Simple user and group management with LDAP integration
|
||||
- Plug-in system
|
||||
|
||||
Following features are not implemented, but we will make them in the future release!
|
||||
|
||||
- Network graph
|
||||
- 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).
|
||||
If you want to try the development version of GitBucket, see [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
|
||||
|
||||
Installation
|
||||
--------
|
||||
GitBucket requires **Java8**. You have to install beforehand when it's not installed.
|
||||
|
||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
|
||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases).
|
||||
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**.
|
||||
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser and logged-in with **root** / **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.
|
||||
|
||||
@@ -47,38 +32,97 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
|
||||
- --host=[HOSTNAME]
|
||||
- --gitbucket.home=[DATA_DIR]
|
||||
|
||||
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
|
||||
To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. 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)
|
||||
About installation on Mac or Windows Server (with IIS), configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
|
||||
|
||||
### Mac OS X
|
||||
#### Installing Via Homebrew
|
||||
Plug-ins
|
||||
--------
|
||||
GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. Some plug-ins are available now:
|
||||
|
||||
$ 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.
|
||||
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
|
||||
- [gitbucket-announce-plugin](https://github.com/gitbucket-plugins/gitbucket-announce-plugin)
|
||||
- [gitbucket-h2-backup-plugin](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin)
|
||||
- [gitbucket-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin)
|
||||
- [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin)
|
||||
- [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-plugin)
|
||||
|
||||
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
|
||||
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
|
||||
|
||||
#### Manual Installation
|
||||
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
|
||||
Support
|
||||
--------
|
||||
|
||||
Run the following commands in `Terminal` to
|
||||
|
||||
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
|
||||
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
|
||||
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue.
|
||||
- Make sure check whether there is a same question or request in the past.
|
||||
- When raise a new issue, write subject in **English** at least.
|
||||
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 3.12 - 27 Feb 2016
|
||||
- New GitHub UI
|
||||
- Improve mobile view
|
||||
- Improve printing style
|
||||
- Individual URL for pull request tabs
|
||||
- SSH host configuration is separated from HTTP base URL
|
||||
|
||||
### 3.11 - 30 Jan 2016
|
||||
- Upgrade Scalatra to 2.4
|
||||
- Sidebar and Footer for Wiki
|
||||
- Branch protection and receive hook extension point for plug-in
|
||||
- Limit recent updated repositories list
|
||||
- Issue actions look-alike GitHub
|
||||
- Web API for labels
|
||||
- Requires Java 8
|
||||
|
||||
### 3.10 - 30 Dec 2015
|
||||
- Move to Bootstrap3
|
||||
- New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`)
|
||||
- Update xsbt-web-plugin
|
||||
- Update H2 database
|
||||
|
||||
### 3.9 - 5 Dec 2015
|
||||
- GFM inline breaks support in Markdown
|
||||
- WebHook on create review comment is available
|
||||
- WebHook event trigger is selectable
|
||||
|
||||
### 3.8 - 31 Oct 2015
|
||||
- Moved to GitHub organization
|
||||
- Omit diff view for large differences
|
||||
- Repository creation API
|
||||
- Render url as link in repository description
|
||||
- Expand attachable file types
|
||||
|
||||
### 3.7 - 3 Oct 2015
|
||||
- Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown
|
||||
- Clone in desktop button
|
||||
- Providing MD5 and SHA-1 checksum for `gitbucket.war` has started
|
||||
|
||||
### 3.6 - 30 Aug 2015
|
||||
- User interface Improvements: Especially, commit list, issues and pull request have been updated largely.
|
||||
- Installed plugins list has been available at the system administration console.
|
||||
- Pages and repository list in the sidebar have been limited and more pages and repositories link is available.
|
||||
- More reference link notation in Markdown has been supported.
|
||||
|
||||
### 3.5 - 1 Aug 2015
|
||||
- Octicons has been applied
|
||||
- Global header has been enhanced. Now it's further similar to GitHub.
|
||||
- Default compare / pull request target has been changed to the parent repository
|
||||
- A lot of updates for [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
|
||||
|
||||
### 3.4 - 27 Jun 2015
|
||||
- Declarative style plug-in definition
|
||||
- New extension point to add markup render
|
||||
- go-import support
|
||||
|
||||
### 3.3 - 31 May 2015
|
||||
- Rich graphical diff for images
|
||||
- File finder is available in the repository viewer
|
||||
- Blame is displayed at the source viewer
|
||||
- Remain user data and repositories even if user is disabled
|
||||
- Mobile view improvement
|
||||
|
||||
### 3.2 - 3 May 2015
|
||||
- Directory history button
|
||||
- Compare / pull request button
|
||||
|
||||
159
build.sbt
Normal file
159
build.sbt
Normal file
@@ -0,0 +1,159 @@
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "3.12.0"
|
||||
val ScalatraVersion = "2.4.0"
|
||||
val JettyVersion = "9.3.6.v20151106"
|
||||
|
||||
lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin)
|
||||
|
||||
sourcesInBase := false
|
||||
organization := Organization
|
||||
name := Name
|
||||
version := GitBucketVersion
|
||||
scalaVersion := "2.11.7"
|
||||
|
||||
// dependency settings
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
|
||||
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
||||
)
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.3.0",
|
||||
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.6",
|
||||
"org.apache.commons" % "commons-compress" % "1.10",
|
||||
"org.apache.commons" % "commons-email" % "1.4",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
||||
"org.apache.sshd" % "apache-sshd" % "1.0.0",
|
||||
"org.apache.tika" % "tika-core" % "1.11",
|
||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.190",
|
||||
"ch.qos.logback" % "logback-classic" % "1.1.1",
|
||||
"com.mchange" % "c3p0" % "0.9.5.2",
|
||||
"com.typesafe" % "config" % "1.3.0",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
"junit" % "junit" % "4.12" % "test",
|
||||
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
|
||||
"org.scalaz" %% "scalaz-core" % "7.2.0" % "test"
|
||||
)
|
||||
|
||||
// Twirl settings
|
||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
|
||||
|
||||
// Compiler settings
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps")
|
||||
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
|
||||
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
|
||||
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")
|
||||
|
||||
// Assembly 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
|
||||
}
|
||||
|
||||
// JRebel
|
||||
jrebel.webLinks += (target in webappPrepare).value
|
||||
jrebel.enabled := System.getenv().get("JREBEL") != null
|
||||
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
|
||||
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
|
||||
}
|
||||
jrebelSettings
|
||||
|
||||
// Create executable war file
|
||||
val executableConfig = config("executable").hide
|
||||
Keys.ivyConfigurations += executableConfig
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||
)
|
||||
|
||||
val executableKey = TaskKey[File]("executable")
|
||||
executableKey := {
|
||||
import org.apache.ivy.util.ChecksumHelper
|
||||
import java.util.jar.{ Manifest => JarManifest }
|
||||
import java.util.jar.Attributes.{ Name => AttrName }
|
||||
|
||||
val workDir = Keys.target.value / "executable"
|
||||
val warName = Keys.name.value + ".war"
|
||||
|
||||
val log = streams.value.log
|
||||
log info s"building executable webapp in ${workDir}"
|
||||
|
||||
// initialize temp directory
|
||||
val temp = workDir / "webapp"
|
||||
IO delete temp
|
||||
|
||||
// include jetty classes
|
||||
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
|
||||
jettyJars foreach { jar =>
|
||||
IO unzip (jar, temp, (name:String) =>
|
||||
(name startsWith "javax/") ||
|
||||
(name startsWith "org/")
|
||||
)
|
||||
}
|
||||
|
||||
// include original war file
|
||||
val warFile = (Keys.`package`).value
|
||||
IO unzip (warFile, temp)
|
||||
|
||||
// include launcher classes
|
||||
val classDir = (Keys.classDirectory in Compile).value
|
||||
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
||||
launchClasses foreach { name =>
|
||||
IO copyFile (classDir / name, temp / name)
|
||||
}
|
||||
|
||||
// zip it up
|
||||
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
||||
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
||||
val manifest = new JarManifest
|
||||
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
||||
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
||||
val outputFile = workDir / warName
|
||||
IO jar (contentMappings, outputFile, manifest)
|
||||
|
||||
// generate checksums
|
||||
Seq("md5", "sha1") foreach { algorithm =>
|
||||
IO.write(
|
||||
workDir / (warName + "." + algorithm),
|
||||
ChecksumHelper computeAsString (outputFile, algorithm)
|
||||
)
|
||||
}
|
||||
|
||||
// done
|
||||
log info s"built executable webapp ${outputFile}"
|
||||
outputFile
|
||||
}
|
||||
/*
|
||||
Keys.artifact in (Compile, executableKey) ~= {
|
||||
_ copy (`type` = "war", extension = "war"))
|
||||
}
|
||||
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
||||
*/
|
||||
61
build.xml
61
build.xml
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<project name="gitbucket" default="all" basedir=".">
|
||||
|
||||
<property name="target.dir" value="target"/>
|
||||
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
|
||||
<property name="jetty.dir" value="embed-jetty"/>
|
||||
<property name="scala.version" value="2.11"/>
|
||||
<property name="gitbucket.version" value="3.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">
|
||||
<os family="windows" />
|
||||
</condition>
|
||||
|
||||
<target name="clean">
|
||||
<delete dir="${embed.classes.dir}"/>
|
||||
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||
</target>
|
||||
|
||||
<target name="war" depends="clean">
|
||||
<exec executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
|
||||
<arg line="clean compile test package" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="embed" depends="war">
|
||||
<mkdir dir="${embed.classes.dir}"/>
|
||||
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
|
||||
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
|
||||
|
||||
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||
basedir="${embed.classes.dir}"
|
||||
update = "true"
|
||||
includes="javax/**,org/**"/>
|
||||
|
||||
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||
basedir="${target.dir}/scala-${scala.version}/classes"
|
||||
update = "true"
|
||||
includes="JettyLauncher.class,HttpsSupportConnector.class"/>
|
||||
</target>
|
||||
|
||||
<target name="rename" depends="embed">
|
||||
<move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
|
||||
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||
</target>
|
||||
|
||||
<target name="all" depends="rename">
|
||||
</target>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -44,7 +44,7 @@ GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
|
||||
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
|
||||
|
||||
# GitBucket version to fetch when installing
|
||||
GITBUCKET_VERSION=2.1
|
||||
GITBUCKET_VERSION=3.5
|
||||
|
||||
#
|
||||
# End of configuration section. Ignore this part
|
||||
|
||||
@@ -38,7 +38,7 @@ 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 wget -qO "$GITBUCKET_WAR_FILE" https://github.com/gitbucket/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
|
||||
|
||||
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Summary: GitHub clone written with Scala.
|
||||
Version: 2.6
|
||||
Release: 1%{?dist}
|
||||
License: Apache
|
||||
URL: https://github.com/takezoe/gitbucket
|
||||
URL: https://github.com/gitbucket/gitbucket
|
||||
Group: System/Servers
|
||||
Source0: %{name}.war
|
||||
Source1: %{name}.init
|
||||
|
||||
60
doc/authenticator.md
Normal file
60
doc/authenticator.md
Normal file
@@ -0,0 +1,60 @@
|
||||
Authentication in Controller
|
||||
========
|
||||
GitBucket provides many [authenticators](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/util/Authenticator.scala) to access controlling in the controller.
|
||||
|
||||
For example, in the case of `RepositoryViwerController`,
|
||||
it references three authenticators: `ReadableUsersAuthenticator`, `ReferrerAuthenticator` and `CollaboratorsAuthenticator`.
|
||||
|
||||
```scala
|
||||
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 with WebHookPullRequestReviewCommentService
|
||||
|
||||
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 with WebHookPullRequestReviewCommentService =>
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Authenticators provides a method to add guard to actions in the controller:
|
||||
|
||||
- `ReadableUsersAuthenticator` provides `readableUsersOnly` method
|
||||
- `ReferrerAuthenticator` provides `referrersOnly` method
|
||||
- `CollaboratorsAuthenticator` provides `collaboratorsOnly` method
|
||||
|
||||
These methods are available in each actions as below:
|
||||
|
||||
```scala
|
||||
// Allows only the repository owner (or manager for group repository) and administrators.
|
||||
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
|
||||
...
|
||||
})
|
||||
|
||||
// Allows only collaborators and administrators.
|
||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||
...
|
||||
})
|
||||
|
||||
// Allows only signed in users which can access the repository.
|
||||
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
Currently, GitBucket provides below authenticators:
|
||||
|
||||
|Trait | Method | Description |
|
||||
|--------------------------|-----------------|--------------------------------------------------------------------------------------|
|
||||
|OneselfAuthenticator |oneselfOnly |Allows only oneself and administrators. |
|
||||
|OwnerAuthenticator |ownerOnly |Allows only the repository owner and administrators. |
|
||||
|UsersAuthenticator |usersOnly |Allows only signed in users. |
|
||||
|AdminAuthenticator |adminOnly |Allows only administrators. |
|
||||
|CollaboratorsAuthenticator|collaboratorsOnly|Allows only collaborators and administrators. |
|
||||
|ReferrerAuthenticator |referrersOnly |Allows only the repository owner (or manager for group repository) and administrators.|
|
||||
|ReadableUsersAuthenticator|readableUsersOnly|Allows only signed in users which can access the repository. |
|
||||
|GroupManagerAuthenticator |managersOnly |Allows only the group managers. |
|
||||
|
||||
Of course, if you make a new plugin, you can define a your own authenticator according to requirement in your plugin.
|
||||
@@ -2,13 +2,13 @@ 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.
|
||||
To release a new version of GitBucket, add the version definition to the [gitbucket.core.servlet.AutoUpdate](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala) at first.
|
||||
|
||||
```scala
|
||||
object AutoUpdate {
|
||||
...
|
||||
/**
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
Version(1, 0)
|
||||
@@ -16,11 +16,11 @@ object AutoUpdate {
|
||||
...
|
||||
```
|
||||
|
||||
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```.
|
||||
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/gitbucket/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:
|
||||
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(
|
||||
|
||||
@@ -1,48 +1,56 @@
|
||||
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.
|
||||
The details are saved at `ISSUE_COMMENT` table.
|
||||
|
||||
To determine if it was any operation, you see the ```ACTION``` column.
|
||||
To determine if it was any operation, you see the `ACTION` column.
|
||||
And in the case of some actions, `CONTENT` column value contains additional information.
|
||||
|
||||
|ACTION|
|
||||
|--------|
|
||||
|comment|
|
||||
|close_comment|
|
||||
|reopen_comment|
|
||||
|close|
|
||||
|reopen|
|
||||
|commit|
|
||||
|merge|
|
||||
|delete_branch|
|
||||
|refer|
|
||||
|ACTION |CONTENT |
|
||||
|---------------|-----------------|
|
||||
|comment |comment |
|
||||
|close_comment |comment |
|
||||
|reopen_comment |comment |
|
||||
|close |"Close" |
|
||||
|reopen |"Reopen" |
|
||||
|commit |comment commitId |
|
||||
|merge |comment |
|
||||
|delete_branch |branchName |
|
||||
|refer |issueId:title |
|
||||
|
||||
### comment
|
||||
|
||||
#####comment
|
||||
This value is saved when users have made a normal comment.
|
||||
|
||||
#####close_comment, reopen_comment
|
||||
### close_comment, reopen_comment
|
||||
|
||||
These values are saved when users have reopened or closed the issue with comments.
|
||||
|
||||
#####close, reopen
|
||||
### 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.
|
||||
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.
|
||||
### 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
|
||||
### merge
|
||||
|
||||
This value is saved when users have merged the pull request.
|
||||
At the same time, store the message to the ```CONTENT``` column.
|
||||
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
|
||||
### 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.
|
||||
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```.
|
||||
### 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`.
|
||||
|
||||
@@ -8,10 +8,10 @@ This directory has following structure:
|
||||
* /HOME/gitbucket
|
||||
* /repositories
|
||||
* /USER_NAME
|
||||
* / REPO_NAME.git (substance of repository. GitServlet sees this directory)
|
||||
* / REPO_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)
|
||||
* /REPO_NAME.wiki.git (wiki repository)
|
||||
* /data
|
||||
* /USER_NAME
|
||||
* /files
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
How to run from the source tree
|
||||
========
|
||||
|
||||
for Testers
|
||||
Run for Development
|
||||
--------
|
||||
|
||||
If you want to test GitBucket, input following command at the root directory of the source tree.
|
||||
|
||||
```
|
||||
C:\gitbucket> sbt ~container:start
|
||||
$ sbt ~jetty: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
|
||||
```
|
||||
Source code modification is detected and reloaded automatically. You can modify logging configuration by editing `src/main/resources/logback-dev.xml`.
|
||||
|
||||
Build war file
|
||||
--------
|
||||
@@ -30,9 +20,15 @@ Build war file
|
||||
To build war file, run the following command:
|
||||
|
||||
```
|
||||
C:\gitbucket> sbt package
|
||||
$ 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.
|
||||
To build executable war file, run
|
||||
|
||||
```
|
||||
$ sbt executable
|
||||
```
|
||||
|
||||
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
|
||||
|
||||
@@ -379,21 +379,21 @@
|
||||
<path d="M588.909,926.673 L560.094,910.545 L564.713,935.73 L588.909,926.673 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.313" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g id="rect3075-11">
|
||||
<path d="M779.562,898.094 C779.396,912.75 779.229,927.406 779.062,942.062 C781.26,942.084 783.458,942.104 785.656,942.125 C784.104,943.688 782.552,945.25 781,946.813 C801.708,967.521 822.417,988.229 843.125,1008.938 C858.531,993.531 873.938,978.125 889.344,962.719 C868.635,942 847.927,921.281 827.219,900.563 C825.49,902.302 823.76,904.042 822.031,905.781 C822.052,903.386 822.073,900.99 822.094,898.594 C807.917,898.427 793.74,898.261 779.563,898.094 z" fill="#FFFFFF"/>
|
||||
<path d="M779.562,898.094 C779.396,912.75 779.229,927.406 779.062,942.062 C781.26,942.084 783.458,942.104 785.656,942.125 C784.104,943.688 782.552,945.25 781,946.813 C801.708,967.521 822.417,988.229 843.125,1008.938 C858.531,993.531 873.938,978.125 889.344,962.719 C868.635,942 847.927,921.281 827.219,900.563 C825.49,902.302 823.76,904.042 822.031,905.781 C822.052,903.386 822.073,900.99 822.094,898.594 C807.917,898.427 793.74,898.261 779.563,898.094 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9" stroke-linecap="round"/>
|
||||
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill="#FFFFFF"/>
|
||||
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9" stroke-linecap="round"/>
|
||||
</g>
|
||||
<path d="M606.483,964.91 L606.483,951.243 L672.089,951.243 L672.089,964.91 z" fill="#B3B3B3" id="rect2995-0-2-8-6"/>
|
||||
<g id="rect3075-11-7">
|
||||
<path d="M786.383,905.075 C786.256,916.229 786.129,927.383 786.003,938.537 C787.675,938.553 789.348,938.568 791.021,938.584 C789.839,939.773 788.658,940.963 787.477,942.152 C803.237,957.911 818.997,973.671 834.756,989.431 C846.481,977.706 858.206,965.982 869.93,954.257 C854.171,938.489 838.411,922.722 822.651,906.954 C821.335,908.278 820.019,909.602 818.703,910.926 C818.719,909.102 818.734,907.279 818.751,905.456 C807.961,905.329 797.172,905.202 786.383,905.075 z" fill="#FFFFFF"/>
|
||||
<path d="M786.383,905.075 C786.256,916.229 786.129,927.383 786.003,938.537 C787.675,938.553 789.348,938.568 791.021,938.584 C789.839,939.773 788.658,940.963 787.477,942.152 C803.237,957.911 818.997,973.671 834.756,989.431 C846.481,977.706 858.206,965.982 869.93,954.257 C854.171,938.489 838.411,922.722 822.651,906.954 C821.335,908.278 820.019,909.602 818.703,910.926 C818.719,909.102 818.734,907.279 818.751,905.456 C807.961,905.329 797.172,905.202 786.383,905.075 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
|
||||
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill="#FFFFFF"/>
|
||||
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g id="path3100-2">
|
||||
<path d="M813.748,916.688 C818.255,921.195 818.255,928.501 813.748,933.008 C809.242,937.514 801.935,937.514 797.429,933.008 C792.922,928.501 792.922,921.195 797.429,916.688 C801.935,912.182 809.242,912.182 813.748,916.688 z" fill="#FFFFFF"/>
|
||||
<path d="M813.748,916.688 C818.255,921.195 818.255,928.501 813.748,933.008 C809.242,937.514 801.935,937.514 797.429,933.008 C792.922,928.501 792.922,921.195 797.429,916.688 C801.935,912.182 809.242,912.182 813.748,916.688 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.585" stroke-linecap="round"/>
|
||||
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill="#FFFFFF"/>
|
||||
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.585" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g id="rect4114">
|
||||
<path d="M813.845,955.107 L834.888,934.064 L864.012,963.188 L842.969,984.231 z" fill="#FFFFFF"/>
|
||||
<path d="M813.845,955.107 L834.888,934.064 L864.012,963.188 L842.969,984.231 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.133"/>
|
||||
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill="#FFFFFF"/>
|
||||
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.133"/>
|
||||
</g>
|
||||
<g id="path2991-7-6">
|
||||
<path d="M969.889,84.636 C969.889,122.652 939.072,153.469 901.056,153.469 C863.04,153.469 832.223,122.652 832.223,84.636 C832.223,46.62 863.04,15.803 901.056,15.803 C939.072,15.803 969.889,46.62 969.889,84.636 z" fill="#A0A0A0"/>
|
||||
@@ -750,5 +750,45 @@
|
||||
</g>
|
||||
<path d="M1396.792,592.168 C1426.908,592.168 1450.613,610.989 1450.613,639.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
|
||||
<path d="M1397.792,545.653 C1453.613,544.493 1499.627,588.735 1499.627,636.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
|
||||
<path d="M871.125,1039.025 C871.125,1039.025 873.794,1016.889 908.043,1011.524 C919.748,1009.691 945.861,1005.107 945.861,978.522" fill-opacity="0" stroke="#A0A0A0" stroke-width="17.059" id="path3207"/>
|
||||
<g id="rect3818-4-8-4">
|
||||
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
|
||||
<g>
|
||||
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
|
||||
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="15"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="path3795-4-8-7-8">
|
||||
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill="#FFFFFF"/>
|
||||
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
|
||||
</g>
|
||||
<g id="path3795-8-4-8">
|
||||
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill="#FFFFFF"/>
|
||||
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
|
||||
</g>
|
||||
<g id="path3795-8-4-8-2">
|
||||
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill="#FFFFFF"/>
|
||||
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
|
||||
</g>
|
||||
<path d="M1114.353,1042.412 C1114.353,1042.412 1117.022,1020.275 1151.271,1014.91 C1162.976,1013.077 1189.089,1008.493 1189.089,981.909" fill-opacity="0" stroke="#000000" stroke-width="17.059" id="path3207"/>
|
||||
<g id="rect3818-4-8-4">
|
||||
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
|
||||
<g>
|
||||
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
|
||||
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill-opacity="0" stroke="#000000" stroke-width="15"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="path3795-4-8-7-8">
|
||||
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill="#FFFFFF"/>
|
||||
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
|
||||
</g>
|
||||
<g id="path3795-8-4-8">
|
||||
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill="#FFFFFF"/>
|
||||
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
|
||||
</g>
|
||||
<g id="path3795-8-4-8-2">
|
||||
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill="#FFFFFF"/>
|
||||
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 82 KiB |
148
doc/jrebel.md
Normal file
148
doc/jrebel.md
Normal file
@@ -0,0 +1,148 @@
|
||||
JRebel integration (optional)
|
||||
=============================
|
||||
|
||||
[JRebel](http://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
|
||||
JRebel is generally able to eliminate the need for the following slow "app restart" in sbt following a code change:
|
||||
|
||||
```
|
||||
> jetty:start
|
||||
```
|
||||
|
||||
While JRebel is not open source, it does reload your code faster than the `~;copy-resources;aux-compile` way of doing things using `sbt`.
|
||||
|
||||
It's only used during development, and doesn't change your deployed app in any way.
|
||||
|
||||
JRebel used to be free for Scala developers, but that changed recently, and now there's a cost associated with usage for Scala. There are trial plans and free non-commercial licenses available if you just want to try it out.
|
||||
|
||||
----
|
||||
|
||||
## 1. Get a JRebel license
|
||||
|
||||
Sign up for a [usage plan](https://my.jrebel.com/). You will need to create an account.
|
||||
|
||||
## 2. Download JRebel
|
||||
|
||||
Download the most recent ["nosetup" JRebel zip](http://zeroturnaround.com/software/jrebel/download/prev-releases/).
|
||||
Next, unzip the downloaded file.
|
||||
|
||||
## 3. Activate
|
||||
|
||||
Follow the [instructions on the JRebel website](http://zeroturnaround.com/software/jrebel/download/prev-releases/) to activate your downloaded JRebel.
|
||||
|
||||
You can use the default settings for all the configurations.
|
||||
|
||||
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
|
||||
|
||||
## 4. Tell jvm where JRebel is
|
||||
|
||||
Fortunately, the gitbucket project is already set up to use JRebel.
|
||||
You only need to tell jvm where to find the jrebel jar.
|
||||
|
||||
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
|
||||
|
||||
```bash
|
||||
export JREBEL=/path/to/jrebel/jrebel.jar
|
||||
```
|
||||
|
||||
For example, if you unzipped your JRebel download in your home directory, you whould use:
|
||||
|
||||
```bash
|
||||
export JREBEL=~/jrebel/jrebel.jar
|
||||
```
|
||||
|
||||
Now reload your shell:
|
||||
|
||||
```
|
||||
$ source ~/.bash_profile # on Mac
|
||||
$ source ~/.bashrc # on Linux
|
||||
```
|
||||
|
||||
## 5. See it in action!
|
||||
|
||||
Now you're ready to use JRebel with the gitbucket.
|
||||
When you run sbt as normal, you will see a long message from JRebel, indicating it has loaded.
|
||||
Here's an abbreviated version of what you will see:
|
||||
|
||||
```
|
||||
$ ./sbt
|
||||
[info] Loading project definition from /git/gitbucket/project
|
||||
[info] Set current project to gitbucket (in build file:/git/gitbucket/)
|
||||
>
|
||||
```
|
||||
|
||||
You will start the servlet container slightly differently now that you're using sbt.
|
||||
|
||||
```
|
||||
> jetty:start
|
||||
:
|
||||
[info] starting server ...
|
||||
[success] Total time: 3 s, completed Jan 3, 2016 9:47:55 PM
|
||||
2016-01-03 21:47:57 JRebel:
|
||||
2016-01-03 21:47:57 JRebel: A newer version '6.3.1' is available for download
|
||||
2016-01-03 21:47:57 JRebel: from http://zeroturnaround.com/software/jrebel/download/
|
||||
2016-01-03 21:47:57 JRebel:
|
||||
2016-01-03 21:47:58 JRebel: Contacting myJRebel server ..
|
||||
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/classes' will be monitored for changes.
|
||||
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/test-classes' will be monitored for changes.
|
||||
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/webapp' will be monitored for changes.
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
2016-01-03 21:48:00 JRebel: #############################################################
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
2016-01-03 21:48:00 JRebel: JRebel Legacy Agent 6.2.5 (201509291538)
|
||||
2016-01-03 21:48:00 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
2016-01-03 21:48:00 JRebel: Over the last 30 days JRebel prevented
|
||||
2016-01-03 21:48:00 JRebel: at least 182 redeploys/restarts saving you about 7.4 hours.
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
2016-01-03 21:48:00 JRebel: Over the last 324 days JRebel prevented
|
||||
2016-01-03 21:48:00 JRebel: at least 1538 redeploys/restarts saving you about 62.4 hours.
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
2016-01-03 21:48:00 JRebel: Licensed to nazo king (using myJRebel).
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
2016-01-03 21:48:00 JRebel: #############################################################
|
||||
2016-01-03 21:48:00 JRebel:
|
||||
:
|
||||
|
||||
> ~ copy-resources
|
||||
[success] Total time: 0 s, completed Jan 3, 2016 9:13:54 PM
|
||||
1. Waiting for source changes... (press enter to interrupt)
|
||||
```
|
||||
|
||||
Finally, change your code.
|
||||
For example, you can change the title on `src/main/twirl/gitbucket/core/main.scala.html` like this:
|
||||
|
||||
```html
|
||||
:
|
||||
<a class="navbar-brand" href="@path/">
|
||||
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
|
||||
@defining(AutoUpdate.getCurrentVersion){ version =>
|
||||
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
|
||||
}
|
||||
change code !!!!!!!!!!!!!!!!
|
||||
</a>
|
||||
:
|
||||
```
|
||||
|
||||
If JRebel is doing is correctly installed you will see a notice for you:
|
||||
|
||||
```
|
||||
1. Waiting for source changes... (press enter to interrupt)
|
||||
2016-01-03 21:48:42 JRebel: Reloading class 'gitbucket.core.html.main$'.
|
||||
[info] Wrote rebel.xml to /git/gitbucket/target/scala-2.11/resource_managed/main/rebel.xml
|
||||
[info] Compiling 1 Scala source to /git/gitbucket/target/scala-2.11/classes...
|
||||
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
|
||||
2. Waiting for source changes... (press enter to interrupt)
|
||||
```
|
||||
|
||||
And you reload browser, JRebel give notice of that it has reloaded classes:
|
||||
|
||||
```
|
||||
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
|
||||
2. Waiting for source changes... (press enter to interrupt)
|
||||
2016-01-03 21:49:13 JRebel: Reloading class 'gitbucket.core.html.main$'.
|
||||
```
|
||||
|
||||
## 6. Limitations
|
||||
|
||||
JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routes patterns, there is nothing JRebel can do, you will have to run `jetty:start`.
|
||||
@@ -3,8 +3,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)
|
||||
* [Authentication in Controller](authenticator.md)
|
||||
* [About Action in Issue Comment](comment_action.md)
|
||||
* [Activity Types](activity.md)
|
||||
* [Notification Email](notification.md)
|
||||
* [Automatic Schema Updating](auto_update.md)
|
||||
* [Release Operation](release.md)
|
||||
* [JRebel integration (optional)](jrebel.md)
|
||||
|
||||
53
doc/release.md
Normal file
53
doc/release.md
Normal file
@@ -0,0 +1,53 @@
|
||||
Release Operation
|
||||
========
|
||||
|
||||
Update version number
|
||||
--------
|
||||
|
||||
Note to update version number in files below:
|
||||
|
||||
### project/build.scala
|
||||
|
||||
```scala
|
||||
object MyBuild extends Build {
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.3.0" // <---- update version!!
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
```
|
||||
|
||||
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
||||
|
||||
```scala
|
||||
object AutoUpdate {
|
||||
|
||||
/**
|
||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 3), // <---- add this line!!
|
||||
new Version(3, 2),
|
||||
```
|
||||
|
||||
Generate release files
|
||||
--------
|
||||
|
||||
Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https://maven.apache.org/).
|
||||
|
||||
### Make release war file
|
||||
|
||||
Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`.
|
||||
|
||||
```bash
|
||||
$sbt executable
|
||||
```
|
||||
|
||||
### Deploy assembly jar file
|
||||
|
||||
For plug-in development, we have to publish the assembly jar file to the public Maven repository by `release/deploy-assembly-jar.sh`.
|
||||
|
||||
```bash
|
||||
$ cd release/
|
||||
$ ./deploy-assembly-jar.sh
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/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/
|
||||
@@ -1 +1 @@
|
||||
sbt.version=0.13.8
|
||||
sbt.version=0.13.9
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import sbt._
|
||||
import Keys._
|
||||
import org.scalatra.sbt._
|
||||
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 = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.2.0"
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
|
||||
lazy val project = Project (
|
||||
"gitbucket",
|
||||
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,8 +1,6 @@
|
||||
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||
|
||||
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")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
|
||||
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
|
||||
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
|
||||
|
||||
24
release/deploy-assembly-jar.sh
Executable file
24
release/deploy-assembly-jar.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
. ./env.sh
|
||||
|
||||
cd ../
|
||||
./sbt.sh clean assembly
|
||||
|
||||
cd release
|
||||
|
||||
if [[ "$GITBUCKET_VERSION" =~ -SNAPSHOT$ ]]; then
|
||||
MVN_DEPLOY_PATH=mvn-snapshot
|
||||
else
|
||||
MVN_DEPLOY_PATH=mvn
|
||||
fi
|
||||
|
||||
echo $MVN_DEPLOY_PATH
|
||||
|
||||
mvn deploy:deploy-file \
|
||||
-DgroupId=gitbucket\
|
||||
-DartifactId=gitbucket-assembly\
|
||||
-Dversion=$GITBUCKET_VERSION\
|
||||
-Dpackaging=jar\
|
||||
-Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\
|
||||
-DrepositoryId=sourceforge.jp\
|
||||
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/$MVN_DEPLOY_PATH/
|
||||
3
release/env.sh
Normal file
3
release/env.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2`
|
||||
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"
|
||||
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 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %*
|
||||
java %JAVA_OPTS% -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.9.jar" %*
|
||||
|
||||
2
sbt.sh
2
sbt.sh
@@ -1,2 +1,2 @@
|
||||
#!/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 "$@"
|
||||
java $JAVA_OPTS -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.9.jar "$@"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
import org.eclipse.jetty.webapp.WebAppContext;
|
||||
|
||||
import java.io.File;
|
||||
@@ -30,16 +29,16 @@ public class JettyLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
Server server = new Server();
|
||||
Server server = new Server(port);
|
||||
|
||||
SelectChannelConnector connector = new SelectChannelConnector();
|
||||
if(host != null) {
|
||||
connector.setHost(host);
|
||||
}
|
||||
connector.setMaxIdleTime(1000 * 60 * 60);
|
||||
connector.setSoLingerTime(-1);
|
||||
connector.setPort(port);
|
||||
server.addConnector(connector);
|
||||
// SelectChannelConnector connector = new SelectChannelConnector();
|
||||
// if(host != null) {
|
||||
// connector.setHost(host);
|
||||
// }
|
||||
// connector.setMaxIdleTime(1000 * 60 * 60);
|
||||
// connector.setSoLingerTime(-1);
|
||||
// connector.setPort(port);
|
||||
// server.addConnector(connector);
|
||||
|
||||
WebAppContext context = new WebAppContext();
|
||||
|
||||
|
||||
26
src/main/resources/logback-dev.xml
Normal file
26
src/main/resources/logback-dev.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>gitbucket.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
<!--
|
||||
<logger name="service.WebHookService" level="DEBUG" />
|
||||
<logger name="servlet" level="DEBUG" />
|
||||
-->
|
||||
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
|
||||
|
||||
</configuration>
|
||||
@@ -6,12 +6,23 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!--
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>gitbucket.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
-->
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
|
||||
<!--
|
||||
<logger name="service.WebHookService" level="DEBUG" />
|
||||
<logger name="servlet" level="DEBUG" />
|
||||
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
|
||||
-->
|
||||
|
||||
</configuration>
|
||||
5
src/main/resources/reference.conf
Normal file
5
src/main/resources/reference.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
c3p0 {
|
||||
privilegeSpawnedThreads=true
|
||||
contextClassLoaderSource=library
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ INSERT INTO ACCOUNT (
|
||||
'root@localhost',
|
||||
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
||||
true,
|
||||
'https://github.com/takezoe/gitbucket',
|
||||
'https://github.com/gitbucket/gitbucket',
|
||||
SYSDATE,
|
||||
SYSDATE,
|
||||
NULL
|
||||
|
||||
25
src/main/resources/update/3_11.sql
Normal file
25
src/main/resources/update/3_11.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
DROP TABLE IF EXISTS PROTECTED_BRANCH;
|
||||
|
||||
CREATE TABLE PROTECTED_BRANCH(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
BRANCH VARCHAR(100) NOT NULL,
|
||||
STATUS_CHECK_ADMIN BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH);
|
||||
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS PROTECTED_BRANCH_REQUIRE_CONTEXT;
|
||||
CREATE TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
BRANCH VARCHAR(100) NOT NULL,
|
||||
CONTEXT VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT);
|
||||
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, BRANCH) REFERENCES PROTECTED_BRANCH (USER_NAME, REPOSITORY_NAME, BRANCH)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
55
src/main/resources/update/3_9.sql
Normal file
55
src/main/resources/update/3_9.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
DROP TABLE IF EXISTS WEB_HOOK_EVENT;
|
||||
|
||||
CREATE TABLE WEB_HOOK_EVENT(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
URL VARCHAR(200) NOT NULL,
|
||||
EVENT VARCHAR(30) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL, EVENT);
|
||||
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, URL) REFERENCES WEB_HOOK (USER_NAME, REPOSITORY_NAME, URL)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE TEMPORARY TABLE TMP_EVENTS (EVENT VARCHAR(30));
|
||||
|
||||
INSERT INTO TMP_EVENTS VALUES ('push'),('issue_comment'),('issues'),('pull_request');
|
||||
|
||||
INSERT INTO WEB_HOOK_EVENT (USER_NAME, REPOSITORY_NAME, URL, EVENT)
|
||||
SELECT USER_NAME, REPOSITORY_NAME, URL, EVENT
|
||||
FROM WEB_HOOK, TMP_EVENTS;
|
||||
|
||||
DROP TABLE TMP_EVENTS;
|
||||
|
||||
ALTER TABLE COMMIT_COMMENT ADD COLUMN ISSUE_ID INT;
|
||||
|
||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||
SELECT
|
||||
A.USER_NAME,
|
||||
A.REPOSITORY_NAME,
|
||||
A.ISSUE_ID,
|
||||
NVL(B.COMMENT_COUNT, 0) + NVL(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
||||
FROM ISSUE A
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) B
|
||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) C
|
||||
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID);
|
||||
|
||||
|
||||
UPDATE COMMIT_COMMENT C SET (ISSUE_ID) = (
|
||||
SELECT MAX(P.ISSUE_ID)
|
||||
FROM PULL_REQUEST P
|
||||
WHERE
|
||||
C.USER_NAME = P.USER_NAME AND
|
||||
C.REPOSITORY_NAME = P.REPOSITORY_NAME AND
|
||||
C.COMMIT_ID = P.COMMIT_ID_TO
|
||||
);
|
||||
|
||||
ALTER TABLE COMMIT_COMMENT DROP COLUMN PULL_REQUEST;
|
||||
@@ -27,7 +27,6 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
}
|
||||
|
||||
context.mount(new IndexController, "/")
|
||||
context.mount(new SearchController, "/")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
context.mount(new DashboardController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
|
||||
16
src/main/scala/gitbucket/core/api/ApiBranch.scala
Normal file
16
src/main/scala/gitbucket/core/api/ApiBranch.scala
Normal file
@@ -0,0 +1,16 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/#get-branch
|
||||
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
|
||||
*/
|
||||
case class ApiBranch(
|
||||
name: String,
|
||||
// commit: ApiBranchCommit,
|
||||
protection: ApiBranchProtection)(repositoryName:RepositoryName) extends FieldSerializable {
|
||||
def _links = Map(
|
||||
"self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"),
|
||||
"html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}"))
|
||||
}
|
||||
47
src/main/scala/gitbucket/core/api/ApiBranchProtection.scala
Normal file
47
src/main/scala/gitbucket/core/api/ApiBranchProtection.scala
Normal file
@@ -0,0 +1,47 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.service.ProtectedBranchService
|
||||
import org.json4s._
|
||||
|
||||
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
||||
case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]){
|
||||
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
|
||||
}
|
||||
|
||||
object ApiBranchProtection{
|
||||
/** form for enabling-and-disabling-branch-protection */
|
||||
case class EnablingAndDisabling(protection: ApiBranchProtection)
|
||||
|
||||
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
|
||||
enabled = info.enabled,
|
||||
required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts)))
|
||||
val statusNone = Status(Off, Seq.empty)
|
||||
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
|
||||
sealed class EnforcementLevel(val name: String)
|
||||
case object Off extends EnforcementLevel("off")
|
||||
case object NonAdmins extends EnforcementLevel("non_admins")
|
||||
case object Everyone extends EnforcementLevel("everyone")
|
||||
object EnforcementLevel {
|
||||
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = if(enabled){
|
||||
if(includeAdministrators){
|
||||
Everyone
|
||||
}else{
|
||||
NonAdmins
|
||||
}
|
||||
}else{
|
||||
Off
|
||||
}
|
||||
}
|
||||
|
||||
implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => (
|
||||
{
|
||||
case JString("off") => Off
|
||||
case JString("non_admins") => NonAdmins
|
||||
case JString("everyone") => Everyone
|
||||
},
|
||||
{
|
||||
case x: EnforcementLevel => JString(x.name)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.IssueComment
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import java.util.Date
|
||||
|
||||
@@ -13,14 +14,16 @@ case class ApiComment(
|
||||
user: ApiUser,
|
||||
body: String,
|
||||
created_at: Date,
|
||||
updated_at: Date)
|
||||
updated_at: Date)(repositoryName: RepositoryName, issueId: Int, isPullRequest: Boolean){
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${issueId}#comment-${id}")
|
||||
}
|
||||
|
||||
object ApiComment{
|
||||
def apply(comment: IssueComment, user: ApiUser): ApiComment =
|
||||
def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser, isPullRequest: Boolean): ApiComment =
|
||||
ApiComment(
|
||||
id = comment.commentId,
|
||||
user = user,
|
||||
body = comment.content,
|
||||
created_at = comment.registeredDate,
|
||||
updated_at = comment.updatedDate)
|
||||
updated_at = comment.updatedDate)(repositoryName, issueId, isPullRequest)
|
||||
}
|
||||
|
||||
@@ -20,13 +20,21 @@ case class ApiCommit(
|
||||
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}")
|
||||
committer: ApiPersonIdent)(repositoryName:RepositoryName, urlIsHtmlUrl: Boolean) extends FieldSerializable{
|
||||
val url = if(urlIsHtmlUrl){
|
||||
ApiPath(s"/${repositoryName.fullName}/commit/${id}")
|
||||
}else{
|
||||
ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
|
||||
}
|
||||
val html_url = if(urlIsHtmlUrl){
|
||||
None
|
||||
}else{
|
||||
Some(ApiPath(s"/${repositoryName.fullName}/commit/${id}"))
|
||||
}
|
||||
}
|
||||
|
||||
object ApiCommit{
|
||||
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = {
|
||||
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): ApiCommit = {
|
||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||
ApiCommit(
|
||||
id = commit.id,
|
||||
@@ -43,6 +51,7 @@ object ApiCommit{
|
||||
},
|
||||
author = ApiPersonIdent.author(commit),
|
||||
committer = ApiPersonIdent.committer(commit)
|
||||
)(repositoryName)
|
||||
)(repositoryName, urlIsHtmlUrl)
|
||||
}
|
||||
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Issue
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import java.util.Date
|
||||
|
||||
@@ -16,10 +17,13 @@ case class ApiIssue(
|
||||
state: String,
|
||||
created_at: Date,
|
||||
updated_at: Date,
|
||||
body: String)
|
||||
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
|
||||
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
|
||||
}
|
||||
|
||||
object ApiIssue{
|
||||
def apply(issue: Issue, user: ApiUser): ApiIssue =
|
||||
def apply(issue: Issue, repositoryName: RepositoryName, user: ApiUser): ApiIssue =
|
||||
ApiIssue(
|
||||
number = issue.issueId,
|
||||
title = issue.title,
|
||||
@@ -27,5 +31,5 @@ object ApiIssue{
|
||||
state = if(issue.closed){ "closed" }else{ "open" },
|
||||
body = issue.content.getOrElse(""),
|
||||
created_at = issue.registeredDate,
|
||||
updated_at = issue.updatedDate)
|
||||
updated_at = issue.updatedDate)(repositoryName, issue.isPullRequest)
|
||||
}
|
||||
|
||||
21
src/main/scala/gitbucket/core/api/ApiLabel.scala
Normal file
21
src/main/scala/gitbucket/core/api/ApiLabel.scala
Normal file
@@ -0,0 +1,21 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Label
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/labels/
|
||||
*/
|
||||
case class ApiLabel(
|
||||
name: String,
|
||||
color: String)(repositoryName: RepositoryName){
|
||||
var url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/labels/${name}")
|
||||
}
|
||||
|
||||
object ApiLabel{
|
||||
def apply(label:Label, repositoryName: RepositoryName): ApiLabel =
|
||||
ApiLabel(
|
||||
name = label.labelName,
|
||||
color = label.color
|
||||
)(repositoryName)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.util.RepositoryName
|
||||
import gitbucket.core.model.CommitComment
|
||||
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
|
||||
*/
|
||||
case class ApiPullRequestReviewComment(
|
||||
id: Int, // 29724692
|
||||
// "diff_hunk": "@@ -1 +1 @@\n-# public-repo",
|
||||
path: String, // "README.md",
|
||||
// "position": 1,
|
||||
// "original_position": 1,
|
||||
commit_id: String, // "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
// "original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
user: ApiUser,
|
||||
body: String, // "Maybe you should use more emojji on this line.",
|
||||
created_at: Date, // "2015-05-05T23:40:27Z",
|
||||
updated_at: Date // "2015-05-05T23:40:27Z",
|
||||
)(repositoryName:RepositoryName, issueId: Int) extends FieldSerializable {
|
||||
// "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692",
|
||||
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/comments/${id}")
|
||||
// "html_url": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692",
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/pull/${issueId}#discussion_r${id}")
|
||||
// "pull_request_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1",
|
||||
val pull_request_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${issueId}")
|
||||
|
||||
/*
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692"
|
||||
},
|
||||
"html": {
|
||||
"href": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692"
|
||||
},
|
||||
"pull_request": {
|
||||
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1"
|
||||
}
|
||||
}
|
||||
*/
|
||||
val _links = Map(
|
||||
"self" -> Map("href" -> url),
|
||||
"html" -> Map("href" -> html_url),
|
||||
"pull_request" -> Map("href" -> pull_request_url))
|
||||
}
|
||||
|
||||
object ApiPullRequestReviewComment{
|
||||
def apply(comment: CommitComment, commentedUser: ApiUser, repositoryName: RepositoryName, issueId: Int): ApiPullRequestReviewComment =
|
||||
new ApiPullRequestReviewComment(
|
||||
id = comment.commentId,
|
||||
path = comment.fileName.getOrElse(""),
|
||||
commit_id = comment.commitId,
|
||||
user = commentedUser,
|
||||
body = comment.content,
|
||||
created_at = comment.registeredDate,
|
||||
updated_at = comment.updatedDate
|
||||
)(repositoryName, issueId)
|
||||
}
|
||||
11
src/main/scala/gitbucket/core/api/ApiPusher.scala
Normal file
11
src/main/scala/gitbucket/core/api/ApiPusher.scala
Normal file
@@ -0,0 +1,11 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Account
|
||||
|
||||
case class ApiPusher(name: String, email: String)
|
||||
|
||||
object ApiPusher {
|
||||
def apply(user: Account): ApiPusher = ApiPusher(
|
||||
name = user.userName,
|
||||
email = user.mailAddress)
|
||||
}
|
||||
@@ -13,10 +13,14 @@ case class ApiRepository(
|
||||
forks: Int,
|
||||
`private`: Boolean,
|
||||
default_branch: String,
|
||||
owner: ApiUser) {
|
||||
owner: ApiUser)(urlIsHtmlUrl: Boolean) {
|
||||
val forks_count = forks
|
||||
val watchers_coun = watchers
|
||||
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
||||
val watchers_count = watchers
|
||||
val url = if(urlIsHtmlUrl){
|
||||
ApiPath(s"/${full_name}")
|
||||
}else{
|
||||
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}")
|
||||
@@ -27,7 +31,8 @@ object ApiRepository{
|
||||
repository: Repository,
|
||||
owner: ApiUser,
|
||||
forkedCount: Int =0,
|
||||
watchers: Int = 0): ApiRepository =
|
||||
watchers: Int = 0,
|
||||
urlIsHtmlUrl: Boolean = false): ApiRepository =
|
||||
ApiRepository(
|
||||
name = repository.repositoryName,
|
||||
full_name = s"${repository.userName}/${repository.repositoryName}",
|
||||
@@ -37,7 +42,7 @@ object ApiRepository{
|
||||
`private` = repository.isPrivate,
|
||||
default_branch = repository.defaultBranch,
|
||||
owner = owner
|
||||
)
|
||||
)(urlIsHtmlUrl)
|
||||
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
|
||||
@@ -45,4 +50,7 @@ object ApiRepository{
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
||||
this(repositoryInfo.repository, ApiUser(owner))
|
||||
|
||||
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ case class ApiUser(
|
||||
|
||||
object ApiUser{
|
||||
def apply(user: Account): ApiUser = ApiUser(
|
||||
login = user.fullName,
|
||||
login = user.userName,
|
||||
email = user.mailAddress,
|
||||
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" },
|
||||
site_admin = user.isAdmin,
|
||||
|
||||
18
src/main/scala/gitbucket/core/api/CreateALabel.scala
Normal file
18
src/main/scala/gitbucket/core/api/CreateALabel.scala
Normal file
@@ -0,0 +1,18 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/labels/#create-a-label
|
||||
* api form
|
||||
*/
|
||||
case class CreateALabel(
|
||||
name: String,
|
||||
color: String
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
name.length<=100 &&
|
||||
!name.startsWith("_") &&
|
||||
!name.startsWith("-") &&
|
||||
color.length==6 &&
|
||||
color.matches("[a-fA-F0-9+_.]+")
|
||||
}
|
||||
}
|
||||
19
src/main/scala/gitbucket/core/api/CreateARepository.scala
Normal file
19
src/main/scala/gitbucket/core/api/CreateARepository.scala
Normal file
@@ -0,0 +1,19 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/#create
|
||||
* api form
|
||||
*/
|
||||
case class CreateARepository(
|
||||
name: String,
|
||||
description: Option[String],
|
||||
`private`: Boolean = false,
|
||||
auto_init: Boolean = false
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
name.length<=40 &&
|
||||
name.matches("[a-zA-Z0-9\\-\\+_.]+") &&
|
||||
!name.startsWith("_") &&
|
||||
!name.startsWith("-")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/** export fields for json */
|
||||
trait FieldSerializable
|
||||
@@ -5,40 +5,50 @@ 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)
|
||||
|
||||
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 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]()
|
||||
|
||||
) + FieldSerializer[ApiUser]() +
|
||||
FieldSerializer[ApiPullRequest]() +
|
||||
FieldSerializer[ApiRepository]() +
|
||||
FieldSerializer[ApiCommitListItem.Parent]() +
|
||||
FieldSerializer[ApiCommitListItem]() +
|
||||
FieldSerializer[ApiCommitListItem.Commit]() +
|
||||
FieldSerializer[ApiCommitStatus]() +
|
||||
FieldSerializer[FieldSerializable]() +
|
||||
FieldSerializer[ApiCombinedCommitStatus]() +
|
||||
FieldSerializer[ApiPullRequest.Commit]() +
|
||||
FieldSerializer[ApiIssue]() +
|
||||
FieldSerializer[ApiComment]() +
|
||||
FieldSerializer[ApiLabel]() +
|
||||
ApiBranchProtection.enforcementLevelSerializer
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
(
|
||||
{
|
||||
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))
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util._
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
@@ -90,8 +90,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
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))),
|
||||
"owner" -> trim(label("Owner" , text(required, maxlength(100), identifier, existsAccount))),
|
||||
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean())),
|
||||
"createReadme" -> trim(label("Create README" , boolean()))
|
||||
@@ -133,7 +133,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
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)),
|
||||
getVisibleRepositories(context.loginAccount, Some(userName)),
|
||||
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
||||
}
|
||||
}
|
||||
@@ -202,16 +202,17 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
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)
|
||||
// // 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)
|
||||
|
||||
removeUserRelatedData(userName)
|
||||
updateAccount(account.copy(isRemoved = true))
|
||||
}
|
||||
|
||||
@@ -365,57 +366,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
*/
|
||||
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)
|
||||
if(getRepository(form.owner, form.name).isEmpty){
|
||||
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||
}
|
||||
|
||||
// redirect to the repository
|
||||
@@ -423,6 +375,54 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Create user repository
|
||||
* https://developer.github.com/v3/repos/#create
|
||||
*/
|
||||
post("/api/v3/user/repos")(usersOnly {
|
||||
val owner = context.loginAccount.get.userName
|
||||
(for {
|
||||
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||
} yield {
|
||||
LockUtil.lock(s"${owner}/${data.name}") {
|
||||
if(getRepository(owner, data.name).isEmpty){
|
||||
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
|
||||
val repository = getRepository(owner, data.name).get
|
||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
|
||||
} else {
|
||||
ApiError(
|
||||
"A repository with this name already exists on this account",
|
||||
Some("https://developer.github.com/v3/repos/#create")
|
||||
)
|
||||
}
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* Create group repository
|
||||
* https://developer.github.com/v3/repos/#create
|
||||
*/
|
||||
post("/api/v3/orgs/:org/repos")(managersOnly {
|
||||
val groupName = params("org")
|
||||
(for {
|
||||
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||
} yield {
|
||||
LockUtil.lock(s"${groupName}/${data.name}") {
|
||||
if(getRepository(groupName, data.name).isEmpty){
|
||||
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
|
||||
val repository = getRepository(groupName, data.name).get
|
||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
|
||||
} else {
|
||||
ApiError(
|
||||
"A repository with this name already exists for this group",
|
||||
Some("https://developer.github.com/v3/repos/#create")
|
||||
)
|
||||
}
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
@@ -447,7 +447,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val accountName = form.accountName
|
||||
|
||||
LockUtil.lock(s"${accountName}/${repository.name}"){
|
||||
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
|
||||
if(getRepository(accountName, repository.name).isDefined ||
|
||||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
|
||||
// redirect to the repository if repository already exists
|
||||
redirect(s"/${accountName}/${repository.name}")
|
||||
@@ -467,6 +467,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
parentUserName = Some(repository.owner)
|
||||
)
|
||||
|
||||
// Add collaborators for group repository
|
||||
val ownerAccount = getAccountByUserName(accountName).get
|
||||
if(ownerAccount.isGroupAccount){
|
||||
getGroupMembers(accountName).foreach { member =>
|
||||
addCollaborator(accountName, repository.name, member.userName)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(accountName, repository.name)
|
||||
|
||||
@@ -488,6 +496,59 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
|
||||
val ownerAccount = getAccountByUserName(owner).get
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
// Insert to the database at first
|
||||
createRepository(name, owner, description, isPrivate)
|
||||
|
||||
// Add collaborators for group repository
|
||||
if(ownerAccount.isGroupAccount){
|
||||
getGroupMembers(owner).foreach { member =>
|
||||
addCollaborator(owner, name, member.userName)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(owner, name)
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(owner, name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
if(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(description.nonEmpty){
|
||||
name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
description.get
|
||||
} else {
|
||||
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, owner, name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(owner, name, loginUserName)
|
||||
}
|
||||
|
||||
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
||||
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
||||
|
||||
@@ -8,7 +8,7 @@ import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util._
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.json4s._
|
||||
import org.scalatra._
|
||||
@@ -28,7 +28,11 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
|
||||
with SystemSettingsService {
|
||||
|
||||
implicit val jsonFormats = DefaultFormats
|
||||
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
|
||||
|
||||
before("/api/v3/*") {
|
||||
contentType = formats("json")
|
||||
}
|
||||
|
||||
// TODO Scala 2.11
|
||||
// // Don't set content type via Accept header.
|
||||
@@ -176,11 +180,17 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
* Context object for the current request.
|
||||
*/
|
||||
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
|
||||
|
||||
val path = settings.baseUrl.getOrElse(request.getContextPath)
|
||||
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
||||
val baseUrl = settings.baseUrl(request)
|
||||
val host = new java.net.URL(baseUrl).getHost
|
||||
val platform = request.getHeader("User-Agent") match {
|
||||
case null => null
|
||||
case agent if agent.contains("Mac") => "mac"
|
||||
case agent if agent.contains("Linux") => "linux"
|
||||
case agent if agent.contains("Win") => "windows"
|
||||
case _ => null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object from cache.
|
||||
|
||||
@@ -94,7 +94,7 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
|
||||
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 userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
html.issues(
|
||||
|
||||
@@ -17,22 +17,22 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||
|
||||
post("/image"){
|
||||
execute { (file, fileId) =>
|
||||
execute({ (file, fileId) =>
|
||||
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
|
||||
session += Keys.Session.Upload(fileId) -> file.name
|
||||
}
|
||||
}, FileUtil.isImage)
|
||||
}
|
||||
|
||||
post("/image/:owner/:repository"){
|
||||
execute { (file, fileId) =>
|
||||
post("/file/:owner/:repository"){
|
||||
execute({ (file, fileId) =>
|
||||
FileUtils.writeByteArrayToFile(new java.io.File(
|
||||
getAttachedDir(params("owner"), params("repository")),
|
||||
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
|
||||
}
|
||||
}, FileUtil.isUploadableType)
|
||||
}
|
||||
|
||||
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
|
||||
case Some(file) if(FileUtil.isImage(file.name)) =>
|
||||
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
|
||||
case Some(file) if(mimeTypeChcker(file.name)) =>
|
||||
defining(FileUtil.generateFileId){ fileId =>
|
||||
f(file, fileId)
|
||||
|
||||
|
||||
@@ -2,35 +2,46 @@ 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.service.{RepositoryService, ActivityService, AccountService, RepositorySearchService, IssuesService}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
|
||||
|
||||
class IndexController extends IndexControllerBase
|
||||
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
|
||||
with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
|
||||
with UsersAuthenticator with ReferrerAuthenticator
|
||||
|
||||
|
||||
trait IndexControllerBase extends ControllerBase {
|
||||
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
|
||||
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
|
||||
with UsersAuthenticator with ReferrerAuthenticator =>
|
||||
|
||||
case class SignInForm(userName: String, password: String)
|
||||
|
||||
val form = mapping(
|
||||
val signinForm = mapping(
|
||||
"userName" -> trim(label("Username", text(required))),
|
||||
"password" -> trim(label("Password", text(required)))
|
||||
)(SignInForm.apply)
|
||||
|
||||
val searchForm = mapping(
|
||||
"query" -> trim(text(required)),
|
||||
"owner" -> trim(text(required)),
|
||||
"repository" -> trim(text(required))
|
||||
)(SearchForm.apply)
|
||||
|
||||
case class SearchForm(query: String, owner: String, repository: String)
|
||||
|
||||
|
||||
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)
|
||||
gitbucket.core.html.index(getRecentActivities(),
|
||||
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||
)
|
||||
} else {
|
||||
val loginUserName = loginAccount.get.userName
|
||||
@@ -39,9 +50,9 @@ trait IndexControllerBase extends ControllerBase {
|
||||
|
||||
visibleOwnerSet ++= loginUserGroups
|
||||
|
||||
html.index(getRecentActivitiesByOwners(visibleOwnerSet),
|
||||
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||
gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet),
|
||||
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -51,10 +62,10 @@ trait IndexControllerBase extends ControllerBase {
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
flash += Keys.Flash.Redirect -> redirect.get
|
||||
}
|
||||
html.signin()
|
||||
gitbucket.core.html.signin()
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
post("/signin", signinForm){ form =>
|
||||
authenticate(context.settings, form.userName, form.password) match {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
@@ -104,7 +115,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
/**
|
||||
* JSON APU for checking user existence.
|
||||
* JSON API for checking user existence.
|
||||
*/
|
||||
post("/_user/existence")(usersOnly {
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
@@ -119,4 +130,33 @@ trait IndexControllerBase extends ControllerBase {
|
||||
// this message is same as github enterprise...
|
||||
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
||||
}
|
||||
|
||||
// TODO Move to RepositoryViwerController?
|
||||
post("/search", searchForm){ form =>
|
||||
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||
}
|
||||
|
||||
// TODO Move to RepositoryViwerController?
|
||||
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
|
||||
val page = try {
|
||||
val i = params.getOrElse("page", "1").toInt
|
||||
if(i <= 0) 1 else i
|
||||
} catch {
|
||||
case e: NumberFormatException => 1
|
||||
}
|
||||
|
||||
target.toLowerCase match {
|
||||
case "issue" => gitbucket.core.search.html.issues(
|
||||
searchIssues(repository.owner, repository.name, query),
|
||||
countFiles(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
|
||||
case _ => gitbucket.core.search.html.code(
|
||||
searchFiles(repository.owner, repository.name, query),
|
||||
countIssues(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import gitbucket.core.util._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.Markdown
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.scalatra.Ok
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
issueId <- params("id").toIntOpt
|
||||
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||
} yield {
|
||||
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, ApiUser(user)) })
|
||||
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
|
||||
}).getOrElse(NotFound)
|
||||
})
|
||||
|
||||
@@ -190,7 +190,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
||||
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||
} yield {
|
||||
JsonFormat(ApiComment(issueComment, ApiUser(context.loginAccount.get)))
|
||||
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
@@ -231,10 +231,20 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
} 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))
|
||||
))
|
||||
Map(
|
||||
"title" -> x.title,
|
||||
"content" -> Markdown.toHtml(
|
||||
markdown = x.content getOrElse "No description given.",
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = isEditable(x.userName, x.repositoryName, x.openedUserName)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
@@ -249,14 +259,30 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
} 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))
|
||||
))
|
||||
Map(
|
||||
"content" -> view.Markdown.toHtml(
|
||||
markdown = x.content,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository =>
|
||||
val labelNames = params("labelNames").split(",")
|
||||
val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName))
|
||||
html.labellist(labels)
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
||||
defining(params("id").toInt){ issueId =>
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||
@@ -326,6 +352,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
||||
case dir if(dir.exists && dir.isDirectory) =>
|
||||
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
||||
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
|
||||
RawData(FileUtil.getMimeType(file.getName), file)
|
||||
}
|
||||
case _ => None
|
||||
@@ -346,11 +373,15 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Same method exists in PullRequestController. Should it moved to IssueService?
|
||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||
if(getIssue(owner, repository, issueId).isDefined){
|
||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
|
||||
fromIssue.issueId + ":" + fromIssue.title, "refer")
|
||||
// Not add if refer comment already exist.
|
||||
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,7 +487,11 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
"issues",
|
||||
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted
|
||||
} else {
|
||||
getCollaborators(owner, repoName)
|
||||
},
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
|
||||
import gitbucket.core.issues.labels.html
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
||||
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||
import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.scalatra.Ok
|
||||
import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok}
|
||||
|
||||
class LabelsController extends LabelsControllerBase
|
||||
with LabelsService with IssuesService with RepositoryService with AccountService
|
||||
@@ -19,7 +20,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
case class LabelForm(labelName: String, color: String)
|
||||
|
||||
val labelForm = mapping(
|
||||
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||
"labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))),
|
||||
"labelColor" -> trim(label("Color", text(required, color)))
|
||||
)(LabelForm.apply)
|
||||
|
||||
@@ -31,6 +32,26 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
/**
|
||||
* List all labels for this repository
|
||||
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
|
||||
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
|
||||
ApiLabel(label, RepositoryName(repository))
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Get a single label
|
||||
* https://developer.github.com/v3/issues/labels/#get-a-single-label
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
|
||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||
JsonFormat(ApiLabel(label, RepositoryName(repository)))
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
||||
html.edit(None, repository)
|
||||
})
|
||||
@@ -45,6 +66,31 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
/**
|
||||
* Create a label
|
||||
* https://developer.github.com/v3/issues/labels/#create-a-label
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
|
||||
(for{
|
||||
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
||||
} yield {
|
||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
||||
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
|
||||
getLabel(repository.owner, repository.name, labelId).map { label =>
|
||||
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
|
||||
} getOrElse NotFound()
|
||||
} else {
|
||||
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
||||
UnprocessableEntity(ApiError(
|
||||
"Validation Failed",
|
||||
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
|
||||
))
|
||||
}
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
||||
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
||||
html.edit(Some(label), repository)
|
||||
@@ -61,11 +107,50 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
/**
|
||||
* Update a label
|
||||
* https://developer.github.com/v3/issues/labels/#update-a-label
|
||||
*/
|
||||
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
||||
(for{
|
||||
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
||||
} yield {
|
||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
||||
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
|
||||
JsonFormat(ApiLabel(
|
||||
getLabel(repository.owner, repository.name, label.labelId).get,
|
||||
RepositoryName(repository)))
|
||||
} else {
|
||||
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
||||
UnprocessableEntity(ApiError(
|
||||
"Validation Failed",
|
||||
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
|
||||
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
||||
Ok()
|
||||
})
|
||||
|
||||
/**
|
||||
* Delete a label
|
||||
* https://developer.github.com/v3/issues/labels/#delete-a-label
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||
deleteLabel(repository.owner, repository.name, label.labelId)
|
||||
NoContent()
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Constraint for the identifier such as user name, repository name or page name.
|
||||
*/
|
||||
@@ -80,4 +165,12 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
private def uniqueLabelName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
|
||||
val owner = params("owner")
|
||||
val repository = params("repository")
|
||||
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 io.github.gitbucket.scalatra.forms._
|
||||
|
||||
class MilestonesController extends MilestonesControllerBase
|
||||
with MilestonesService with RepositoryService with AccountService
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
|
||||
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
|
||||
import gitbucket.core.pulls.html
|
||||
import gitbucket.core.service.CommitStatusService
|
||||
import gitbucket.core.service.MergeService
|
||||
@@ -16,7 +16,7 @@ import gitbucket.core.util._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -27,13 +27,13 @@ import scala.collection.JavaConverters._
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with CommitStatusService with MergeService
|
||||
with CommitStatusService with MergeService with ProtectedBranchService
|
||||
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with CommitStatusService with MergeService =>
|
||||
with CommitStatusService with MergeService with ProtectedBranchService =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||
|
||||
@@ -46,7 +46,10 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||
"commitIdTo" -> trim(text(required, maxlength(40)))
|
||||
"commitIdTo" -> trim(text(required, maxlength(40))),
|
||||
"assignedUserName" -> trim(optional(text())),
|
||||
"milestoneId" -> trim(optional(number())),
|
||||
"labelNames" -> trim(optional(text()))
|
||||
)(PullRequestForm.apply)
|
||||
|
||||
val mergeForm = mapping(
|
||||
@@ -62,7 +65,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
requestRepositoryName: String,
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String)
|
||||
commitIdTo: String,
|
||||
assignedUserName: Option[String],
|
||||
milestoneId: Option[Int],
|
||||
labelNames: Option[String]
|
||||
)
|
||||
|
||||
case class MergeForm(message: String)
|
||||
|
||||
@@ -112,7 +119,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
commits,
|
||||
diffs,
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
repository)
|
||||
repository,
|
||||
flash.toMap.map(f => f._1 -> f._2.toString))
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
@@ -129,7 +137,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||
} yield {
|
||||
JsonFormat(ApiPullRequest(
|
||||
issue,
|
||||
@@ -159,24 +167,36 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo)
|
||||
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
|
||||
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
|
||||
checkConflict(owner, name, pullreq.branch, issueId)
|
||||
}
|
||||
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
|
||||
val hasMergePermission = hasWritePermission(owner, name, context.loginAccount)
|
||||
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
|
||||
val mergeStatus = PullRequestService.MergeStatus(
|
||||
hasConflict = hasConflict,
|
||||
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
|
||||
branchProtection = branchProtection,
|
||||
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
|
||||
needStatusCheck = context.loginAccount.map{ u =>
|
||||
branchProtection.needStatusCheck(u.userName)
|
||||
}.getOrElse(true),
|
||||
hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
|
||||
context.loginAccount.map{ u =>
|
||||
!getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName)
|
||||
}.getOrElse(false),
|
||||
hasMergePermission = hasMergePermission,
|
||||
commitIdTo = pullreq.commitIdTo)
|
||||
html.mergeguide(
|
||||
hasConfrict,
|
||||
hasProblem,
|
||||
mergeStatus,
|
||||
issue,
|
||||
pullreq,
|
||||
statuses,
|
||||
repository,
|
||||
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
@@ -196,6 +216,75 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository =>
|
||||
(for {
|
||||
issueId <- params("id").toIntOpt
|
||||
loginAccount <- context.loginAccount
|
||||
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
|
||||
owner = pullreq.requestUserName
|
||||
name = pullreq.requestRepositoryName
|
||||
if hasWritePermission(owner, name, context.loginAccount)
|
||||
} yield {
|
||||
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
|
||||
if(branchProtection.needStatusCheck(loginAccount.userName)){
|
||||
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
|
||||
} else {
|
||||
val repository = getRepository(owner, name).get
|
||||
LockUtil.lock(s"${owner}/${name}"){
|
||||
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
|
||||
pullreq.branch
|
||||
}else{
|
||||
s"${pullreq.userName}:${pullreq.branch}"
|
||||
}
|
||||
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
|
||||
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
|
||||
"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
||||
case None => // conflict
|
||||
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
|
||||
case Some(oldId) =>
|
||||
// update pull request
|
||||
updatePullRequests(owner, name, pullreq.requestBranch)
|
||||
|
||||
using(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
|
||||
// after update branch
|
||||
|
||||
val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}")
|
||||
val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList
|
||||
|
||||
commits.foreach{ commit =>
|
||||
if(!existIds.contains(commit.id)){
|
||||
createIssueComment(owner, name, commit)
|
||||
}
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordPushActivity(owner, name, loginAccount.userName, pullreq.branch, commits)
|
||||
|
||||
// close issue by commit message
|
||||
if(pullreq.requestBranch == repository.repository.defaultBranch){
|
||||
commits.map{ commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHookByRequestBranch("synchronize", repository, pullreq.requestBranch, baseUrl, loginAccount)
|
||||
callWebHookOf(owner, name, WebHook.Push) {
|
||||
for {
|
||||
ownerAccount <- getAccountByUserName(owner)
|
||||
} yield {
|
||||
WebHookService.WebHookPushPayload(git, loginAccount, pullreq.requestBranch, repository, commits, ownerAccount, oldId = oldId, newId = newCommitId)
|
||||
}
|
||||
}
|
||||
}
|
||||
flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}"
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -221,7 +310,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
|
||||
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
|
||||
if(pullreq.branch == defaultBranch){
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
@@ -232,6 +321,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||
}
|
||||
|
||||
updatePullRequests(owner, name, pullreq.branch)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
@@ -251,7 +343,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val headBranch:Option[String] = params.get("head")
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
||||
getRepository(originUserName, originRepositoryName).map { originRepository =>
|
||||
using(
|
||||
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
@@ -281,14 +373,23 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner){
|
||||
originRepositoryName <- if(originOwner == forkedOwner) {
|
||||
// Self repository
|
||||
Some(forkedRepository.name)
|
||||
} else if(forkedRepository.repository.originUserName.isEmpty){
|
||||
// when ForkedRepository is the original repository
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
|
||||
// Original repository
|
||||
forkedRepository.repository.originRepositoryName
|
||||
} else {
|
||||
forkedRepository.repository.originRepositoryName.orElse {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
// Sibling repository
|
||||
getUserRepositories(originOwner).find { x =>
|
||||
x.repository.originUserName == forkedRepository.repository.originUserName &&
|
||||
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
|
||||
}.map(_.repository.repositoryName)
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
@@ -301,32 +402,44 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
originRepository.owner, originRepository.name, originId,
|
||||
forkedRepository.owner, forkedRepository.name, forkedId)
|
||||
|
||||
(oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId))
|
||||
(Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
} else {
|
||||
// Commit id
|
||||
(oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId))
|
||||
(Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
}
|
||||
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner, originRepository.name, oldId.getName,
|
||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||
(oldId, newId) match {
|
||||
case (Some(oldId), Some(newId)) => {
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner, originRepository.name, oldId.getName,
|
||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||
|
||||
html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
},
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
|
||||
html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
},
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount),
|
||||
(getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted,
|
||||
getMilestones(originRepository.owner, originRepository.name),
|
||||
getLabels(originRepository.owner, originRepository.name)
|
||||
)
|
||||
}
|
||||
case (oldId, newId) =>
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
|
||||
s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
|
||||
s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}")
|
||||
}
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
@@ -344,7 +457,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
@@ -362,47 +475,78 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
val writable = hasWritePermission(owner, name, context.loginAccount)
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
|
||||
val issueId = createIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginUserName,
|
||||
title = form.title,
|
||||
content = form.content,
|
||||
assignedUserName = None,
|
||||
milestoneId = None,
|
||||
isPullRequest = true)
|
||||
val issueId = createIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginUserName,
|
||||
title = form.title,
|
||||
content = form.content,
|
||||
assignedUserName = if(writable) form.assignedUserName else None,
|
||||
milestoneId = if(writable) form.milestoneId else None,
|
||||
isPullRequest = true)
|
||||
|
||||
createPullRequest(
|
||||
originUserName = repository.owner,
|
||||
originRepositoryName = repository.name,
|
||||
issueId = issueId,
|
||||
originBranch = form.targetBranch,
|
||||
requestUserName = form.requestUserName,
|
||||
requestRepositoryName = form.requestRepositoryName,
|
||||
requestBranch = form.requestBranch,
|
||||
commitIdFrom = form.commitIdFrom,
|
||||
commitIdTo = form.commitIdTo)
|
||||
createPullRequest(
|
||||
originUserName = repository.owner,
|
||||
originRepositoryName = repository.name,
|
||||
issueId = issueId,
|
||||
originBranch = form.targetBranch,
|
||||
requestUserName = form.requestUserName,
|
||||
requestRepositoryName = form.requestRepositoryName,
|
||||
requestBranch = form.requestBranch,
|
||||
commitIdFrom = form.commitIdFrom,
|
||||
commitIdTo = form.commitIdTo)
|
||||
|
||||
// fetch requested branch
|
||||
fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
||||
// insert labels
|
||||
if(writable){
|
||||
form.labelNames.map { value =>
|
||||
val labels = getLabels(owner, name)
|
||||
value.split(",").foreach { labelName =>
|
||||
labels.find(_.labelName == labelName).map { label =>
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
|
||||
// fetch requested branch
|
||||
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
// record activity
|
||||
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
|
||||
|
||||
// notifications
|
||||
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}")
|
||||
// call web hook
|
||||
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
getIssue(owner, name, issueId.toString) foreach { issue =>
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
})
|
||||
|
||||
// TODO Same method exists in IssueController. Should it moved to IssueService?
|
||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||
if(getIssue(owner, repository, issueId).isDefined){
|
||||
// Not add if refer comment already exist.
|
||||
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
@@ -453,7 +597,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
"pulls",
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted
|
||||
} else {
|
||||
getCollaborators(owner, repoName)
|
||||
},
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
|
||||
@@ -462,4 +610,15 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
repository,
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
}
|
||||
|
||||
// TODO: same as gitbucket.core.servlet.CommitLogHook ...
|
||||
private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = {
|
||||
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
||||
if(getIssue(owner, repository, issueId).isDefined){
|
||||
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
||||
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,38 +2,45 @@ 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.{RepositoryService, AccountService, WebHookService, ProtectedBranchService, CommitStatusService}
|
||||
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 io.github.gitbucket.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
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
|
||||
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
with RepositoryService with AccountService with WebHookService
|
||||
with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
|
||||
with OwnerAuthenticator with UsersAuthenticator
|
||||
|
||||
trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with WebHookService
|
||||
self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
|
||||
with OwnerAuthenticator with UsersAuthenticator =>
|
||||
|
||||
// for repository options
|
||||
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||
case class OptionsForm(repositoryName: String, description: Option[String], isPrivate: Boolean)
|
||||
|
||||
val optionsForm = mapping(
|
||||
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||
"repositoryName" -> trim(label("Repository Name", 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 default branch
|
||||
case class DefaultBranchForm(defaultBranch: String)
|
||||
|
||||
val defaultBranchForm = mapping(
|
||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
|
||||
)(DefaultBranchForm.apply)
|
||||
|
||||
// for collaborator addition
|
||||
case class CollaboratorForm(userName: String)
|
||||
|
||||
@@ -42,10 +49,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
)(CollaboratorForm.apply)
|
||||
|
||||
// for web hook url addition
|
||||
case class WebHookForm(url: String)
|
||||
case class WebHookForm(url: String, events: Set[WebHook.Event])
|
||||
|
||||
val webHookForm = mapping(
|
||||
"url" -> trim(label("url", text(required, webHook)))
|
||||
def webHookForm(update:Boolean) = mapping(
|
||||
"url" -> trim(label("url", text(required, webHook(update)))),
|
||||
"events" -> webhookEvents
|
||||
)(WebHookForm.apply)
|
||||
|
||||
// for transfer ownership
|
||||
@@ -73,12 +81,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* 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
|
||||
@@ -96,14 +102,61 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
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")
|
||||
})
|
||||
|
||||
|
||||
/** branch settings */
|
||||
get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
|
||||
val protecteions = getProtectedBranchList(repository.owner, repository.name)
|
||||
html.branches(repository, protecteions, flash.get("info"))
|
||||
});
|
||||
|
||||
/** Update default branch */
|
||||
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
|
||||
if(repository.branchList.find(_ == form.defaultBranch).isEmpty){
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
||||
} else {
|
||||
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
|
||||
// Change repository HEAD
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + form.defaultBranch)
|
||||
}
|
||||
flash += "info" -> "Repository default branch has been updated."
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
|
||||
}
|
||||
})
|
||||
|
||||
/** Branch protection for branch */
|
||||
get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository =>
|
||||
import gitbucket.core.api._
|
||||
val branch = params("branch")
|
||||
if(repository.branchList.find(_ == branch).isEmpty){
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
|
||||
} else {
|
||||
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
|
||||
val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).toSet
|
||||
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
|
||||
html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
|
||||
}
|
||||
})
|
||||
|
||||
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
||||
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
|
||||
import gitbucket.core.api._
|
||||
(for{
|
||||
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
|
||||
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
|
||||
} yield {
|
||||
if(protection.enabled){
|
||||
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
|
||||
} else {
|
||||
disableBranchProtection(repository.owner, repository.name, branch)
|
||||
}
|
||||
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the Collaborators page.
|
||||
*/
|
||||
@@ -138,14 +191,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* 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"))
|
||||
html.hooks(getWebHooks(repository.owner, repository.name), repository, flash.get("info"))
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the web hook edit page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
||||
val webhook = WebHook(repository.owner, repository.name, "")
|
||||
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
|
||||
})
|
||||
|
||||
/**
|
||||
* Add the web hook URL.
|
||||
*/
|
||||
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
|
||||
addWebHookURL(repository.owner, repository.name, form.url)
|
||||
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
||||
addWebHook(repository.owner, repository.name, form.url, form.events)
|
||||
flash += "info" -> s"Webhook ${form.url} created"
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
@@ -153,30 +215,87 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Delete the web hook URL.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
|
||||
deleteWebHookURL(repository.owner, repository.name, params("url"))
|
||||
deleteWebHook(repository.owner, repository.name, params("url"))
|
||||
flash += "info" -> s"Webhook ${params("url")} deleted"
|
||||
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) =>
|
||||
ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
|
||||
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => Array(h.getName, h.getValue) }
|
||||
|
||||
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(_))
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent._
|
||||
import scala.util.control.NonFatal
|
||||
import org.apache.http.util.EntityUtils
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
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)
|
||||
val url = params("url")
|
||||
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
|
||||
val dummyPayload = {
|
||||
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||
.setMaxCount(4)
|
||||
.call.iterator.asScala.map(new CommitInfo(_)).toList
|
||||
val pushedCommit = commits.drop(1)
|
||||
|
||||
WebHookPushPayload(
|
||||
git = git,
|
||||
sender = ownerAccount,
|
||||
refName = "refs/heads/" + repository.repository.defaultBranch,
|
||||
repositoryInfo = repository,
|
||||
commits = pushedCommit,
|
||||
repositoryOwner = ownerAccount,
|
||||
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
|
||||
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId())
|
||||
)
|
||||
}
|
||||
flash += "url" -> form.url
|
||||
flash += "info" -> "Test payload deployed!"
|
||||
|
||||
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
|
||||
|
||||
val toErrorMap: PartialFunction[Throwable, Map[String,String]] = {
|
||||
case e: java.net.UnknownHostException => Map("error"-> ("Unknown host " + e.getMessage))
|
||||
case e: java.lang.IllegalArgumentException => Map("error"-> ("invalid url"))
|
||||
case e: org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url"))
|
||||
case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage))
|
||||
}
|
||||
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(Map(
|
||||
"url" -> url,
|
||||
"request" -> Await.result(reqFuture.map(req => Map(
|
||||
"headers" -> _headers(req.getAllHeaders),
|
||||
"payload" -> json
|
||||
)).recover(toErrorMap), 20 seconds),
|
||||
"responce" -> Await.result(resFuture.map(res => Map(
|
||||
"status" -> res.getStatusLine(),
|
||||
"body" -> EntityUtils.toString(res.getEntity()),
|
||||
"headers" -> _headers(res.getAllHeaders())
|
||||
)).recover(toErrorMap), 20 seconds)
|
||||
))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the web hook edit page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
|
||||
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
|
||||
html.edithooks(webhook, events, repository, flash.get("info"), false)
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* Update web hook settings.
|
||||
*/
|
||||
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
|
||||
updateWebHook(repository.owner, repository.name, form.url, form.events)
|
||||
flash += "info" -> s"webhook ${form.url} updated"
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
@@ -226,9 +345,30 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
/**
|
||||
* Provides duplication check for web hook url.
|
||||
*/
|
||||
private def webHook: Constraint = new Constraint(){
|
||||
private def webHook(needExists: Boolean): 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.")
|
||||
if(getWebHook(params("owner"), params("repository"), value).isDefined != needExists){
|
||||
Some(if(needExists){
|
||||
"URL had not been registered yet."
|
||||
} else {
|
||||
"URL had been registered already."
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
private def webhookEvents = new ValueType[Set[WebHook.Event]]{
|
||||
def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
|
||||
WebHook.Event.values.flatMap { t =>
|
||||
params.get(name + "." + t.name).map(_ => t)
|
||||
}.toSet
|
||||
}
|
||||
def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
|
||||
Seq(name -> messages("error.required").format(name))
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.repo.html
|
||||
import gitbucket.core.helper
|
||||
import gitbucket.core.service._
|
||||
@@ -10,17 +13,17 @@ 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.model.{Account, CommitState, WebHook}
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.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.errors.MissingObjectException
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import org.eclipse.jgit.treewalk._
|
||||
@@ -30,8 +33,7 @@ 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
|
||||
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
@@ -39,7 +41,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
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 =>
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService =>
|
||||
|
||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||
@@ -101,11 +103,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
*/
|
||||
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))
|
||||
helpers.markdown(
|
||||
markdown = params("content"),
|
||||
repository = repository,
|
||||
enableWikiLink = params("enableWikiLink").toBoolean,
|
||||
enableRefsLink = params("enableRefsLink").toBoolean,
|
||||
enableLineBreaks = params("enableLineBreaks").toBoolean,
|
||||
enableTaskList = params("enableTaskList").toBoolean,
|
||||
enableAnchor = false,
|
||||
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -176,7 +183,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
*
|
||||
* 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 =>
|
||||
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
|
||||
(for{
|
||||
ref <- params.get("ref")
|
||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||
@@ -187,6 +194,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||
*
|
||||
* legacy route
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
|
||||
listStatusesRoute.action()
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
||||
*
|
||||
@@ -205,12 +221,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
||||
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
|
||||
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
|
||||
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")),
|
||||
protectedBranch)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
|
||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
||||
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||
@@ -218,7 +237,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
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))
|
||||
JGitUtil.getContentInfo(git, path, objectId),
|
||||
protectedBranch)
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
@@ -249,7 +269,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
||||
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
|
||||
}")
|
||||
})
|
||||
|
||||
@@ -270,7 +290,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
||||
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
|
||||
}")
|
||||
})
|
||||
|
||||
@@ -281,53 +301,109 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/raw/*")(referrersOnly { repository =>
|
||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||
getPathObjectId(git, path, revCommit).flatMap { objectId =>
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
contentType = FileUtil.getMimeType(path)
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
loader.copyTo(response.outputStream)
|
||||
()
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the file content of the specified branch or commit.
|
||||
*/
|
||||
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
||||
val blobRoute = 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)
|
||||
// Download (This route is left for backword compatibility)
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
contentType = FileUtil.getMimeType(path)
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
loader.copyTo(response.outputStream)
|
||||
()
|
||||
} 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))
|
||||
html.blob(id, repository, path.split("/").toList,
|
||||
JGitUtil.getContentInfo(git, path, objectId),
|
||||
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
request.paths(2) == "blame")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/blame/*"){
|
||||
blobRoute.action()
|
||||
}
|
||||
|
||||
/**
|
||||
* Blame data.
|
||||
*/
|
||||
ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository =>
|
||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||
contentType = formats("json")
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
|
||||
Map(
|
||||
"root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}",
|
||||
"id" -> id,
|
||||
"path" -> path,
|
||||
"last" -> last,
|
||||
"blame" -> JGitUtil.getBlame(git, id, path).map{ blame =>
|
||||
Map(
|
||||
"id" -> blame.id,
|
||||
"author" -> view.helpers.user(blame.authorName, blame.authorEmailAddress).toString,
|
||||
"avatar" -> view.helpers.avatarLink(blame.authorName, 32, blame.authorEmailAddress).toString,
|
||||
"authed" -> helper.html.datetimeago(blame.authorTime).toString,
|
||||
"prev" -> blame.prev,
|
||||
"prevPath" -> blame.prevPath,
|
||||
"commited" -> blame.commitTime.getTime,
|
||||
"message" -> blame.message,
|
||||
"lines" -> blame.lines)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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))
|
||||
try {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e:MissingObjectException => NotFound
|
||||
}
|
||||
})
|
||||
|
||||
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.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
|
||||
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)
|
||||
@@ -352,13 +428,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
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.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
|
||||
val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get
|
||||
form.issueId match {
|
||||
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||
case Some(issueId) =>
|
||||
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
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)
|
||||
helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
|
||||
@@ -370,8 +448,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
} 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))
|
||||
Map(
|
||||
"content" -> view.Markdown.toHtml(
|
||||
markdown = x.content,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
|
||||
)
|
||||
))
|
||||
}
|
||||
} else Unauthorized
|
||||
@@ -403,10 +489,17 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
* 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
|
||||
val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet
|
||||
val branches = JGitUtil.getBranches(
|
||||
owner = repository.owner,
|
||||
name = repository.name,
|
||||
defaultBranch = repository.repository.defaultBranch,
|
||||
origin = repository.repository.originUserName.isEmpty
|
||||
)
|
||||
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
||||
.map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name)))
|
||||
.reverse
|
||||
|
||||
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||
})
|
||||
|
||||
@@ -467,14 +560,42 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
html.forked(
|
||||
getRepository(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name),
|
||||
context.baseUrl),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||
getForkedRepositories(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||
repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the file find of branch.
|
||||
*/
|
||||
get("/:owner/:repository/find/*")(referrersOnly { repository =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val ref = multiParams("splat").head
|
||||
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
||||
html.find(ref,
|
||||
treeId,
|
||||
repository,
|
||||
context.loginAccount match {
|
||||
case None => List()
|
||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||
})
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all file list of branch.
|
||||
*/
|
||||
ajaxGet("/:owner/:repository/tree-list/:tree")(referrersOnly { repository =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val treeId = params("tree")
|
||||
contentType = formats("json")
|
||||
Map("paths" -> JGitUtil.getAllFileListByTreeId(git, treeId))
|
||||
}
|
||||
})
|
||||
|
||||
private def splitPath(repository: RepositoryService.RepositoryInfo, path: String): (String, String) = {
|
||||
val id = repository.branchList.collectFirst {
|
||||
case branch if(path == branch || path.startsWith(branch + "/")) => branch
|
||||
@@ -486,7 +607,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
|
||||
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
|
||||
private val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
|
||||
s"readme.${extension}"
|
||||
} ++ Seq("readme.txt", "readme")
|
||||
|
||||
/**
|
||||
* Provides HTML of the file list.
|
||||
@@ -510,7 +633,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
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)
|
||||
!file.isDirectory && readmeFiles.contains(file.name.toLowerCase)
|
||||
}.map { file =>
|
||||
val path = (file.name :: parentPath.reverse).reverse
|
||||
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
|
||||
@@ -564,7 +687,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
headName, loginAccount.fullName, loginAccount.mailAddress, message)
|
||||
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
inserter.close()
|
||||
|
||||
// update refs
|
||||
val refUpdate = git.getRepository.updateRef(headName)
|
||||
@@ -587,9 +710,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
// 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") {
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
|
||||
getAccountByUserName(repository.owner).map{ ownerAccount =>
|
||||
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)
|
||||
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
|
||||
oldId = headTip, newId = commitId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -634,11 +758,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
.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
|
||||
|
||||
|
||||
|
||||
override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.search.html
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
|
||||
import ControlUtil._
|
||||
import Implicits._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SearchController extends SearchControllerBase
|
||||
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
|
||||
|
||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
|
||||
|
||||
val searchForm = mapping(
|
||||
"query" -> trim(text(required)),
|
||||
"owner" -> trim(text(required)),
|
||||
"repository" -> trim(text(required))
|
||||
)(SearchForm.apply)
|
||||
|
||||
case class SearchForm(query: String, owner: String, repository: String)
|
||||
|
||||
post("/search", searchForm){ form =>
|
||||
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||
}
|
||||
|
||||
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
|
||||
val page = try {
|
||||
val i = params.getOrElse("page", "1").toInt
|
||||
if(i <= 0) 1 else i
|
||||
} catch {
|
||||
case e: NumberFormatException => 1
|
||||
}
|
||||
|
||||
target.toLowerCase match {
|
||||
case "issue" => html.issues(
|
||||
searchIssues(repository.owner, repository.name, query),
|
||||
countFiles(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
|
||||
case _ => html.code(
|
||||
searchFiles(repository.owner, repository.name, query),
|
||||
countIssues(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import gitbucket.core.admin.html
|
||||
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
||||
import gitbucket.core.util.AdminAuthenticator
|
||||
import gitbucket.core.ssh.SshServer
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import SystemSettingsService._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with AdminAuthenticator
|
||||
@@ -23,8 +24,10 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||
"ssh" -> trim(label("SSH access", boolean())),
|
||||
"sshHost" -> trim(label("SSH host", optional(text()))),
|
||||
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||
"smtp" -> optionalIfNotChecked("notification", mapping(
|
||||
"useSMTP" -> trim(label("SMTP", boolean())),
|
||||
"smtp" -> optionalIfNotChecked("useSMTP", mapping(
|
||||
"host" -> trim(label("SMTP Host", text(required))),
|
||||
"port" -> trim(label("SMTP Port", optional(number()))),
|
||||
"user" -> trim(label("SMTP User", optional(text()))),
|
||||
@@ -49,9 +52,14 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
"keystore" -> trim(label("Keystore", optional(text())))
|
||||
)(Ldap.apply))
|
||||
)(SystemSettings.apply).verifying { settings =>
|
||||
if(settings.ssh && settings.baseUrl.isEmpty){
|
||||
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
||||
} else Nil
|
||||
Vector(
|
||||
if(settings.ssh && settings.baseUrl.isEmpty){
|
||||
Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
||||
} else None,
|
||||
if(settings.ssh && settings.sshHost.isEmpty){
|
||||
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
|
||||
} else None
|
||||
).flatten
|
||||
}
|
||||
|
||||
private val pluginForm = mapping(
|
||||
@@ -67,20 +75,21 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
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){
|
||||
if (form.sshAddress != context.settings.sshAddress) {
|
||||
SshServer.stop()
|
||||
for {
|
||||
sshAddress <- form.sshAddress
|
||||
baseUrl <- form.baseUrl
|
||||
}
|
||||
SshServer.start(sshAddress, baseUrl)
|
||||
}
|
||||
|
||||
flash += "info" -> "System settings has been updated."
|
||||
redirect("/admin/system")
|
||||
})
|
||||
|
||||
get("/admin/plugins")(adminOnly {
|
||||
html.plugins(PluginRegistry().getPlugins())
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 io.github.gitbucket.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
@@ -100,12 +100,12 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
deleteRepository(userName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 io.github.gitbucket.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
@@ -38,7 +38,9 @@ trait WikiControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
||||
getWikiPage(repository.owner, repository.name, "Home").map { page =>
|
||||
html.page("Home", page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
getWikiPage(repository.owner, repository.name, "_Sidebar"),
|
||||
getWikiPage(repository.owner, repository.name, "_Footer"))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
|
||||
})
|
||||
|
||||
@@ -47,7 +49,9 @@ trait WikiControllerBase extends ControllerBase {
|
||||
|
||||
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
||||
html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
getWikiPage(repository.owner, repository.name, "_Sidebar"),
|
||||
getWikiPage(repository.owner, repository.name, "_Footer"))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
|
||||
})
|
||||
|
||||
@@ -124,7 +128,11 @@ trait WikiControllerBase extends ControllerBase {
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||
if(notReservedPageName(form.pageName)) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||
} else {
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -140,7 +148,11 @@ trait WikiControllerBase extends ControllerBase {
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||
if(notReservedPageName(form.pageName)) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||
} else {
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -186,13 +198,15 @@ trait WikiControllerBase extends ControllerBase {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if(value.exists("\\/:*?\"<>|".contains(_))){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
} else if(notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))){
|
||||
Some(s"${name} starts with invalid character.")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
private def notReservedPageName(value: String) = ! (Array[String]("_Sidebar","_Footer") contains value)
|
||||
|
||||
private def conflictForNew: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
targetWikiPage.map { _ =>
|
||||
|
||||
@@ -26,12 +26,16 @@ protected[model] trait TemplateComponent { self: Profile =>
|
||||
|
||||
trait LabelTemplate extends BasicTemplate { self: Table[_] =>
|
||||
val labelId = column[Int]("LABEL_ID")
|
||||
val labelName = column[String]("LABEL_NAME")
|
||||
|
||||
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)
|
||||
|
||||
def byLabel(owner: String, repository: String, labelName: String) =
|
||||
byRepository(owner, repository) && (this.labelName === labelName.bind)
|
||||
}
|
||||
|
||||
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
|
||||
@@ -54,4 +58,9 @@ protected[model] trait TemplateComponent { self: Profile =>
|
||||
byRepository(userName, repositoryName) && (this.commitId === commitId)
|
||||
}
|
||||
|
||||
trait BranchTemplate extends BasicTemplate{ self: Table[_] =>
|
||||
val branch = column[String]("BRANCH")
|
||||
def byBranch(owner: String, repository: String, branchName: String) = byRepository(owner, repository) && (branch === branchName.bind)
|
||||
def byBranch(owner: Column[String], repository: Column[String], branchName: Column[String]) = byRepository(owner, repository) && (this.branch === branchName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile =>
|
||||
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
|
||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
val pullRequest = column[Boolean]("PULL_REQUEST")
|
||||
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply)
|
||||
val issueId = column[Option[Int]]("ISSUE_ID")
|
||||
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, issueId) <> (CommitComment.tupled, CommitComment.unapply)
|
||||
|
||||
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
||||
}
|
||||
@@ -74,5 +74,5 @@ case class CommitComment(
|
||||
newLine: Option[Int],
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date,
|
||||
pullRequest: Boolean
|
||||
issueId: Option[Int]
|
||||
) extends Comment
|
||||
|
||||
@@ -19,7 +19,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile =>
|
||||
val creator = column[String]("CREATOR")
|
||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> (CommitStatus.tupled, CommitStatus.unapply)
|
||||
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> ((CommitStatus.apply _).tupled, CommitStatus.unapply)
|
||||
def byPrimaryKey(id: Int) = commitStatusId === id.bind
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,20 @@ case class CommitStatus(
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date
|
||||
)
|
||||
|
||||
object CommitStatus {
|
||||
def pending(owner: String, repository: String, context: String) = CommitStatus(
|
||||
commitStatusId = 0,
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
commitId = "",
|
||||
context = context,
|
||||
state = CommitState.PENDING,
|
||||
targetUrl = None,
|
||||
description = Some("Waiting for status to be reported"),
|
||||
creator = "",
|
||||
registeredDate = new java.util.Date(),
|
||||
updatedDate = new java.util.Date())
|
||||
}
|
||||
|
||||
sealed abstract class CommitState(val name: String)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ trait LabelComponent extends TemplateComponent { self: Profile =>
|
||||
|
||||
class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate {
|
||||
override val labelId = column[Int]("LABEL_ID", O AutoInc)
|
||||
val labelName = column[String]("LABEL_NAME")
|
||||
override val labelName = column[String]("LABEL_NAME")
|
||||
val color = column[String]("COLOR")
|
||||
def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ trait CoreProfile extends ProfileProvider with Profile
|
||||
with RepositoryComponent
|
||||
with SshKeyComponent
|
||||
with WebHookComponent
|
||||
with WebHookEventComponent
|
||||
with PluginComponent
|
||||
with ProtectedBranchComponent
|
||||
|
||||
object Profile extends CoreProfile
|
||||
|
||||
37
src/main/scala/gitbucket/core/model/ProtectedBranch.scala
Normal file
37
src/main/scala/gitbucket/core/model/ProtectedBranch.scala
Normal file
@@ -0,0 +1,37 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
import scala.slick.lifted.MappedTo
|
||||
import scala.slick.jdbc._
|
||||
|
||||
trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
import self._
|
||||
|
||||
lazy val ProtectedBranches = TableQuery[ProtectedBranches]
|
||||
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
|
||||
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN")
|
||||
def * = (userName, repositoryName, branch, statusCheckAdmin) <> (ProtectedBranch.tupled, ProtectedBranch.unapply)
|
||||
def byPrimaryKey(userName: String, repositoryName: String, branch: String) = byBranch(userName, repositoryName, branch)
|
||||
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], branch: Column[String]) = byBranch(userName, repositoryName, branch)
|
||||
}
|
||||
|
||||
lazy val ProtectedBranchContexts = TableQuery[ProtectedBranchContexts]
|
||||
class ProtectedBranchContexts(tag: Tag) extends Table[ProtectedBranchContext](tag, "PROTECTED_BRANCH_REQUIRE_CONTEXT") with BranchTemplate {
|
||||
val context = column[String]("CONTEXT")
|
||||
def * = (userName, repositoryName, branch, context) <> (ProtectedBranchContext.tupled, ProtectedBranchContext.unapply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case class ProtectedBranch(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
statusCheckAdmin: Boolean)
|
||||
|
||||
|
||||
case class ProtectedBranchContext(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
context: String)
|
||||
@@ -7,7 +7,7 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
|
||||
|
||||
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||
val url = column[String]("URL")
|
||||
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
|
||||
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||
}
|
||||
@@ -18,3 +18,32 @@ case class WebHook(
|
||||
repositoryName: String,
|
||||
url: String
|
||||
)
|
||||
|
||||
object WebHook {
|
||||
sealed class Event(var name: String)
|
||||
case object CommitComment extends Event("commit_comment")
|
||||
case object Create extends Event("create")
|
||||
case object Delete extends Event("delete")
|
||||
case object Deployment extends Event("deployment")
|
||||
case object DeploymentStatus extends Event("deployment_status")
|
||||
case object Fork extends Event("fork")
|
||||
case object Gollum extends Event("gollum")
|
||||
case object IssueComment extends Event("issue_comment")
|
||||
case object Issues extends Event("issues")
|
||||
case object Member extends Event("member")
|
||||
case object PageBuild extends Event("page_build")
|
||||
case object Public extends Event("public")
|
||||
case object PullRequest extends Event("pull_request")
|
||||
case object PullRequestReviewComment extends Event("pull_request_review_comment")
|
||||
case object Push extends Event("push")
|
||||
case object Release extends Event("release")
|
||||
case object Status extends Event("status")
|
||||
case object TeamAdd extends Event("team_add")
|
||||
case object Watch extends Event("watch")
|
||||
object Event{
|
||||
val values = List(CommitComment,Create,Delete,Deployment,DeploymentStatus,Fork,Gollum,IssueComment,Issues,Member,PageBuild,Public,PullRequest,PullRequestReviewComment,Push,Release,Status,TeamAdd,Watch)
|
||||
private val map:Map[String,Event] = values.map(e => e.name -> e).toMap
|
||||
def valueOf(name: String): Event = map(name)
|
||||
def valueOpt(name: String): Option[Event] = map.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
30
src/main/scala/gitbucket/core/model/WebHookEvent.scala
Normal file
30
src/main/scala/gitbucket/core/model/WebHookEvent.scala
Normal file
@@ -0,0 +1,30 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait WebHookEventComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
import gitbucket.core.model.Profile.WebHooks
|
||||
|
||||
lazy val WebHookEvents = TableQuery[WebHookEvents]
|
||||
|
||||
implicit val typedType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
|
||||
|
||||
class WebHookEvents(tag: Tag) extends Table[WebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate {
|
||||
val url = column[String]("URL")
|
||||
val event = column[WebHook.Event]("EVENT")
|
||||
def * = (userName, repositoryName, url, event) <> ((WebHookEvent.apply _).tupled, WebHookEvent.unapply)
|
||||
|
||||
def byWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||
def byWebHook(owner: Column[String], repository: Column[String], url: Column[String]) =
|
||||
byRepository(userName, repositoryName) && (this.url === url)
|
||||
def byWebHook(webhook: WebHooks) =
|
||||
byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url)
|
||||
def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byWebHook(owner, repository, url) && (this.event === event.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class WebHookEvent(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
url: String,
|
||||
event: WebHook.Event
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import gitbucket.core.model.Session
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
|
||||
/**
|
||||
* Define the Git repository routing.
|
||||
*
|
||||
* @param urlPattern the regular expression which matches the repository path (e.g. "gist/(.+?)/(.+?)\\.git")
|
||||
* @param localPath the string to assemble local file path of repository (e.g. "gist/$1/$2")
|
||||
* @param filter the filter for request to the Git repository which is defined by this routing
|
||||
*/
|
||||
case class GitRepositoryRouting(urlPattern: String, localPath: String, filter: GitRepositoryFilter){
|
||||
|
||||
def this(urlPattern: String, localPath: String) = {
|
||||
this(urlPattern, localPath, new GitRepositoryFilter(){
|
||||
def filter(repositoryName: String, userName: Option[String], settings: SystemSettings, isUpdating: Boolean)
|
||||
(implicit session: Session): Boolean = true
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters request to plug-in served repository. This is used to provide authentication mainly.
|
||||
*/
|
||||
trait GitRepositoryFilter {
|
||||
|
||||
/**
|
||||
* Filters request to Git repository. If this method returns true then request is accepted.
|
||||
*
|
||||
* @param path the repository path which starts with '/'
|
||||
* @param userName the authenticated user name or None
|
||||
* @param settings the system settings
|
||||
* @param isUpdating true if update request, otherwise false
|
||||
* @param session the database session
|
||||
* @return true if allow accessing to repository, otherwise false.
|
||||
*/
|
||||
def filter(path: String, userName: Option[String], settings: SystemSettings, isUpdating: Boolean)
|
||||
(implicit session: Session): Boolean
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import javax.servlet.ServletContext
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Version
|
||||
|
||||
/**
|
||||
@@ -15,16 +17,105 @@ trait Plugin {
|
||||
val description: String
|
||||
val versions: Seq[Version]
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides images.
|
||||
*/
|
||||
val images: Seq[(String, Array[Byte])] = Nil
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides images.
|
||||
*/
|
||||
def images(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, Array[Byte])] = Nil
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides controllers.
|
||||
*/
|
||||
val controllers: Seq[(String, ControllerBase)] = Nil
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides controllers.
|
||||
*/
|
||||
def controllers(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, ControllerBase)] = Nil
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides JavaScript.
|
||||
*/
|
||||
val javaScripts: Seq[(String, String)] = Nil
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides JavaScript.
|
||||
*/
|
||||
def javaScripts(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, String)] = Nil
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides renderers.
|
||||
*/
|
||||
val renderers: Seq[(String, Renderer)] = Nil
|
||||
|
||||
/**
|
||||
* Override to declare this plug-in provides renderers.
|
||||
*/
|
||||
def renderers(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, Renderer)] = Nil
|
||||
|
||||
/**
|
||||
* Override to add git repository routings.
|
||||
*/
|
||||
val repositoryRoutings: Seq[GitRepositoryRouting] = Nil
|
||||
|
||||
/**
|
||||
* Override to add git repository routings.
|
||||
*/
|
||||
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
|
||||
|
||||
/**
|
||||
* Override to add receive hooks.
|
||||
*/
|
||||
val receiveHooks: Seq[ReceiveHook] = Nil
|
||||
|
||||
/**
|
||||
* Override to add receive hooks.
|
||||
*/
|
||||
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
|
||||
|
||||
/**
|
||||
* This method is invoked in initialization of plugin system.
|
||||
* Register plugin functionality to PluginRegistry.
|
||||
*/
|
||||
def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit
|
||||
def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {
|
||||
(images ++ images(registry, context, settings)).foreach { case (id, in) =>
|
||||
registry.addImage(id, in)
|
||||
}
|
||||
(controllers ++ controllers(registry, context, settings)).foreach { case (path, controller) =>
|
||||
registry.addController(path, controller)
|
||||
}
|
||||
(javaScripts ++ javaScripts(registry, context, settings)).foreach { case (path, script) =>
|
||||
registry.addJavaScript(path, script)
|
||||
}
|
||||
(renderers ++ renderers(registry, context, settings)).foreach { case (extension, renderer) =>
|
||||
registry.addRenderer(extension, renderer)
|
||||
}
|
||||
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
|
||||
registry.addRepositoryRouting(routing)
|
||||
}
|
||||
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
|
||||
registry.addReceiveHook(receiveHook)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked in shutdown of plugin system.
|
||||
* If the plugin has any resources, release them in this method.
|
||||
*/
|
||||
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit
|
||||
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
|
||||
|
||||
/**
|
||||
* Helper method to get a resource from classpath.
|
||||
*/
|
||||
protected def fromClassPath(path: String): Array[Byte] =
|
||||
using(getClass.getClassLoader.getResourceAsStream(path)){ in =>
|
||||
val bytes = new Array[Byte](in.available)
|
||||
in.read(bytes)
|
||||
bytes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import javax.servlet.ServletContext
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
|
||||
import gitbucket.core.controller.{Context, ControllerBase}
|
||||
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
@@ -24,6 +25,13 @@ class PluginRegistry {
|
||||
private val javaScripts = new ListBuffer[(String, String)]
|
||||
private val controllers = new ListBuffer[(ControllerBase, String)]
|
||||
private val images = mutable.Map[String, String]()
|
||||
private val renderers = mutable.Map[String, Renderer]()
|
||||
renderers ++= Seq(
|
||||
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
|
||||
)
|
||||
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
|
||||
private val receiveHooks = new ListBuffer[ReceiveHook]
|
||||
receiveHooks += new ProtectedBranchReceiveHook()
|
||||
|
||||
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
||||
plugins += pluginInfo
|
||||
@@ -31,34 +39,74 @@ class PluginRegistry {
|
||||
|
||||
def getPlugins(): List[PluginInfo] = plugins.toList
|
||||
|
||||
def addImage(id: String, bytes: Array[Byte]): Unit = {
|
||||
val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false))
|
||||
images += ((id, encoded))
|
||||
}
|
||||
|
||||
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
|
||||
def addImage(id: String, in: InputStream): Unit = {
|
||||
val bytes = using(in){ in =>
|
||||
val bytes = new Array[Byte](in.available)
|
||||
in.read(bytes)
|
||||
bytes
|
||||
}
|
||||
val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false))
|
||||
images += ((id, encoded))
|
||||
addImage(id, bytes)
|
||||
}
|
||||
|
||||
def getImage(id: String): String = images(id)
|
||||
|
||||
def addController(controller: ControllerBase, path: String): Unit = {
|
||||
def addController(path: String, controller: ControllerBase): Unit = {
|
||||
controllers += ((controller, path))
|
||||
}
|
||||
|
||||
def getControllers(): List[(ControllerBase, String)] = controllers.toList
|
||||
|
||||
def addJavaScript(path: String, script: String): Unit = {
|
||||
javaScripts += Tuple2(path, script)
|
||||
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
|
||||
def addController(controller: ControllerBase, path: String): Unit = {
|
||||
addController(path, controller)
|
||||
}
|
||||
|
||||
//def getJavaScripts(): List[(String, String)] = javaScripts.toList
|
||||
def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq
|
||||
|
||||
def addJavaScript(path: String, script: String): Unit = {
|
||||
javaScripts += ((path, script))
|
||||
}
|
||||
|
||||
def getJavaScript(currentPath: String): List[String] = {
|
||||
javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
|
||||
}
|
||||
|
||||
def addRenderer(extension: String, renderer: Renderer): Unit = {
|
||||
renderers += ((extension, renderer))
|
||||
}
|
||||
|
||||
def getRenderer(extension: String): Renderer = {
|
||||
renderers.get(extension).getOrElse(DefaultRenderer)
|
||||
}
|
||||
|
||||
def renderableExtensions: Seq[String] = renderers.keys.toSeq
|
||||
|
||||
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = {
|
||||
repositoryRoutings += routing
|
||||
}
|
||||
|
||||
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = {
|
||||
repositoryRoutings.toSeq
|
||||
}
|
||||
|
||||
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
|
||||
PluginRegistry().getRepositoryRoutings().find {
|
||||
case GitRepositoryRouting(urlPath, _, _) => {
|
||||
repositoryPath.matches("/" + urlPath + "(/.*)?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addReceiveHook(commitHook: ReceiveHook): Unit = {
|
||||
receiveHooks += commitHook
|
||||
}
|
||||
|
||||
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
||||
|
||||
private case class GlobalAction(
|
||||
method: String,
|
||||
path: String,
|
||||
@@ -130,7 +178,7 @@ object PluginRegistry {
|
||||
))
|
||||
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
case e: Throwable => {
|
||||
logger.error(s"Error during plugin initialization", e)
|
||||
}
|
||||
}
|
||||
|
||||
15
src/main/scala/gitbucket/core/plugin/ReceiveHook.scala
Normal file
15
src/main/scala/gitbucket/core/plugin/ReceiveHook.scala
Normal file
@@ -0,0 +1,15 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import gitbucket.core.model.Profile._
|
||||
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
|
||||
import profile.simple._
|
||||
|
||||
trait ReceiveHook {
|
||||
|
||||
def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
|
||||
(implicit session: Session): Option[String] = None
|
||||
|
||||
def postReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
|
||||
(implicit session: Session): Unit = ()
|
||||
|
||||
}
|
||||
54
src/main/scala/gitbucket/core/plugin/Renderer.scala
Normal file
54
src/main/scala/gitbucket/core/plugin/Renderer.scala
Normal file
@@ -0,0 +1,54 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.service.RepositoryService
|
||||
import gitbucket.core.view.Markdown
|
||||
import play.twirl.api.Html
|
||||
|
||||
/**
|
||||
* A render engine to render content to HTML.
|
||||
*/
|
||||
trait Renderer {
|
||||
|
||||
/**
|
||||
* Render the given request to HTML.
|
||||
*/
|
||||
def render(request: RenderRequest): Html
|
||||
|
||||
}
|
||||
|
||||
object MarkdownRenderer extends Renderer {
|
||||
override def render(request: RenderRequest): Html = {
|
||||
import request._
|
||||
Html(Markdown.toHtml(
|
||||
markdown = fileContent,
|
||||
repository = repository,
|
||||
enableWikiLink = enableWikiLink,
|
||||
enableRefsLink = enableRefsLink,
|
||||
enableAnchor = enableAnchor,
|
||||
enableLineBreaks = false
|
||||
)(context))
|
||||
}
|
||||
}
|
||||
|
||||
object DefaultRenderer extends Renderer {
|
||||
override def render(request: RenderRequest): Html = {
|
||||
import request._
|
||||
Html(
|
||||
s"<tt>${
|
||||
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
|
||||
}</tt>"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
case class RenderRequest(
|
||||
filePath: List[String],
|
||||
fileContent: String,
|
||||
branch: String,
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean,
|
||||
enableRefsLink: Boolean,
|
||||
enableAnchor: Boolean,
|
||||
context: Context
|
||||
)
|
||||
@@ -7,46 +7,51 @@ import gitbucket.core.model.{CommitState, CommitStatus, Account}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
|
||||
import org.joda.time.LocalDateTime
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
|
||||
trait CommitStatusService {
|
||||
/** insert or update */
|
||||
def createCommitStatus(userName: String, repositoryName: String, sha:String, context:String, state:CommitState, targetUrl:Option[String], description:Option[String], now:java.util.Date, creator:Account)(implicit s: Session): Int =
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind )
|
||||
def createCommitStatus(userName: String, repositoryName: String, sha: String, context: String, state: CommitState,
|
||||
targetUrl: Option[String], description: Option[String], now: java.util.Date, creator: Account)(implicit s: Session): Int =
|
||||
CommitStatuses
|
||||
.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind )
|
||||
.map(_.commitStatusId).firstOption match {
|
||||
case Some(id:Int) => {
|
||||
CommitStatuses.filter(_.byPrimaryKey(id)).map{
|
||||
t => (t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
|
||||
}.update( (state, targetUrl, now, creator.userName, description) )
|
||||
id
|
||||
}
|
||||
case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus(
|
||||
userName = userName,
|
||||
repositoryName = repositoryName,
|
||||
commitId = sha,
|
||||
context = context,
|
||||
state = state,
|
||||
targetUrl = targetUrl,
|
||||
description = description,
|
||||
creator = creator.userName,
|
||||
registeredDate = now,
|
||||
updatedDate = now)
|
||||
case Some(id: Int) => {
|
||||
CommitStatuses.filter(_.byPrimaryKey(id)).map { t =>
|
||||
(t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
|
||||
}.update((state, targetUrl, now, creator.userName, description))
|
||||
id
|
||||
}
|
||||
case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus(
|
||||
userName = userName,
|
||||
repositoryName = repositoryName,
|
||||
commitId = sha,
|
||||
context = context,
|
||||
state = state,
|
||||
targetUrl = targetUrl,
|
||||
description = description,
|
||||
creator = creator.userName,
|
||||
registeredDate = now,
|
||||
updatedDate = now)
|
||||
}
|
||||
|
||||
def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session) :Option[CommitStatus] =
|
||||
CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption
|
||||
|
||||
def getCommitStatus(userName: String, repositoryName: String, sha: String, context: String)(implicit s: Session) :Option[CommitStatus] =
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind ).firstOption
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind).firstOption
|
||||
|
||||
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
|
||||
byCommitStatues(userName, repositoryName, sha).list
|
||||
|
||||
def getRecentStatuesContexts(userName: String, repositoryName: String, time: java.util.Date)(implicit s: Session) :List[String] =
|
||||
CommitStatuses.filter(t => t.byRepository(userName, repositoryName)).filter(t => t.updatedDate > time.bind).groupBy(_.context).map(_._1).list
|
||||
|
||||
def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] =
|
||||
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts)
|
||||
.filter{ case (t,a) => t.creator === a.userName }.list
|
||||
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts).filter { case (t, a) => t.creator === a.userName }.list
|
||||
|
||||
protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) ).sortBy(_.updatedDate desc)
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha)).sortBy(_.updatedDate desc)
|
||||
|
||||
}
|
||||
@@ -13,9 +13,9 @@ import StringUtil._
|
||||
|
||||
trait CommitsService {
|
||||
|
||||
def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) =
|
||||
def getCommitComments(owner: String, repository: String, commitId: String, includePullRequest: Boolean)(implicit s: Session) =
|
||||
CommitComments filter {
|
||||
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
|
||||
t => t.byCommit(owner, repository, commitId) && (t.issueId.isEmpty || includePullRequest)
|
||||
} list
|
||||
|
||||
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||
@@ -27,7 +27,8 @@ trait CommitsService {
|
||||
None
|
||||
|
||||
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
|
||||
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int =
|
||||
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int],
|
||||
issueId: Option[Int])(implicit s: Session): Int =
|
||||
CommitComments.autoInc insert CommitComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
@@ -39,7 +40,7 @@ trait CommitsService {
|
||||
newLine = newLine,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
pullRequest = pullRequest)
|
||||
issueId = issueId)
|
||||
|
||||
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
|
||||
CommitComments
|
||||
|
||||
@@ -22,11 +22,13 @@ trait IssuesService {
|
||||
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||
IssueComments filter (_.byIssue(owner, repository, issueId)) list
|
||||
|
||||
/** @return IssueComment and commentedUser */
|
||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] =
|
||||
/** @return IssueComment and commentedUser and Issue */
|
||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account, Issue)] =
|
||||
IssueComments.filter(_.byIssue(owner, repository, issueId))
|
||||
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
|
||||
.innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName )
|
||||
.innerJoin(Issues).on{ case ((t1, t2), t3) => t3.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
|
||||
.map{ case ((t1, t2), t3) => (t1, t2, t3) }
|
||||
.list
|
||||
|
||||
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||
@@ -90,7 +92,7 @@ trait IssuesService {
|
||||
def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={
|
||||
if(issueList.isEmpty){
|
||||
Map.empty
|
||||
}else{
|
||||
} else {
|
||||
import scala.slick.jdbc._
|
||||
val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ")
|
||||
implicit val qset = SetParameter[Seq[(String, String, Int)]] {
|
||||
@@ -397,7 +399,7 @@ trait IssuesService {
|
||||
object IssuesService {
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
val IssueLimit = 30
|
||||
val IssueLimit = 25
|
||||
|
||||
case class IssueSearchCondition(
|
||||
labels: Set[String] = Set.empty,
|
||||
@@ -474,9 +476,11 @@ object IssuesService {
|
||||
* Restores IssueSearchCondition instance from filter query.
|
||||
*/
|
||||
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
|
||||
val conditions = filter.split("[ \t]+").map { x =>
|
||||
val dim = x.split(":")
|
||||
dim(0) -> dim(1)
|
||||
val conditions = filter.split("[ \t]+").flatMap { x =>
|
||||
x.split(":") match {
|
||||
case Array(key, value) => Some((key, value))
|
||||
case _ => None
|
||||
}
|
||||
}.groupBy(_._1).map { case (key, values) =>
|
||||
key -> values.map(_._2).toSeq
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ trait LabelsService {
|
||||
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
|
||||
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
|
||||
|
||||
def getLabel(owner: String, repository: String, labelName: String)(implicit s: Session): Option[Label] =
|
||||
Labels.filter(_.byLabel(owner, repository, labelName)).firstOption
|
||||
|
||||
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
|
||||
Labels returning Labels.map(_.labelId) += Label(
|
||||
userName = owner,
|
||||
|
||||
@@ -10,10 +10,9 @@ import org.eclipse.jgit.merge.MergeStrategy
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository}
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
|
||||
|
||||
trait MergeService {
|
||||
import MergeService._
|
||||
/**
|
||||
@@ -52,26 +51,30 @@ trait MergeService {
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||
*/
|
||||
def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${branch}"
|
||||
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
|
||||
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
|
||||
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${remoteBranch}"
|
||||
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
|
||||
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||
try {
|
||||
// fetch objects from origin repository branch
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||
.setRemote(getRepositoryDir(remoteUserName, remoteRepositoryName).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 mergeBaseTip = git.getRepository.resolve(s"refs/heads/${localBranch}")
|
||||
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
if(merger.merge(mergeBaseTip, mergeTip)){
|
||||
Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
case e: NoMergeBaseException => None
|
||||
}
|
||||
} finally {
|
||||
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||
@@ -80,8 +83,54 @@ trait MergeService {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||
*/
|
||||
def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean =
|
||||
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty
|
||||
|
||||
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
|
||||
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
|
||||
loginAccount: Account, message: String): Option[ObjectId] = {
|
||||
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) =>
|
||||
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
|
||||
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId))
|
||||
Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
|
||||
}
|
||||
oldBaseId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
object MergeService{
|
||||
object Util{
|
||||
// return treeId
|
||||
def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = {
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(treeId)
|
||||
mergeCommit.setParentIds(parents:_*)
|
||||
mergeCommit.setAuthor(committer)
|
||||
mergeCommit.setCommitter(committer)
|
||||
mergeCommit.setMessage(message)
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = repository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.close()
|
||||
mergeCommitId
|
||||
}
|
||||
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = {
|
||||
// update refs
|
||||
val refUpdate = repository.updateRef(ref)
|
||||
refUpdate.setNewObjectId(newObjectId)
|
||||
refUpdate.setForceUpdate(force)
|
||||
refUpdate.setRefLogIdent(committer)
|
||||
refLogMessage.map(refUpdate.setRefLogMessage(_, true))
|
||||
refUpdate.update()
|
||||
}
|
||||
}
|
||||
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
|
||||
val repository = git.getRepository
|
||||
val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||
@@ -90,17 +139,17 @@ object MergeService{
|
||||
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
|
||||
def checkConflictCache(): Option[Boolean] = {
|
||||
Option(repository.resolve(mergedBranchName)).flatMap{ merged =>
|
||||
if(parseCommit( merged ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
|
||||
if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){
|
||||
// merged branch exists
|
||||
Some(false)
|
||||
}else{
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
|
||||
if(parseCommit( conflicted ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
|
||||
if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){
|
||||
// conflict branch exists
|
||||
Some(true)
|
||||
}else{
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
@@ -120,17 +169,12 @@ object MergeService{
|
||||
def updateBranch(treeId:ObjectId, message:String, branchName:String){
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(treeId, committer, message)
|
||||
// update refs
|
||||
val refUpdate = repository.updateRef(branchName)
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.setRefLogIdent(committer)
|
||||
refUpdate.update()
|
||||
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
|
||||
}
|
||||
if(!conflicted){
|
||||
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
|
||||
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
|
||||
}else{
|
||||
} else {
|
||||
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
|
||||
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
|
||||
}
|
||||
@@ -145,28 +189,12 @@ object MergeService{
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
||||
// update refs
|
||||
val refUpdate = repository.updateRef(s"refs/heads/${branch}")
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(false)
|
||||
refUpdate.setRefLogIdent(committer)
|
||||
refUpdate.setRefLogMessage("merged", true)
|
||||
refUpdate.update()
|
||||
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
|
||||
}
|
||||
// return treeId
|
||||
private def createMergeCommit(treeId:ObjectId, committer:PersonIdent, message:String) = {
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(treeId)
|
||||
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
||||
mergeCommit.setAuthor(committer)
|
||||
mergeCommit.setCommitter(committer)
|
||||
mergeCommit.setMessage(message)
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = repository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
mergeCommitId
|
||||
}
|
||||
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
|
||||
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
|
||||
|
||||
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model._
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.plugin.ReceiveHook
|
||||
import profile.simple._
|
||||
|
||||
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
|
||||
|
||||
|
||||
trait ProtectedBranchService {
|
||||
import ProtectedBranchService._
|
||||
private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] =
|
||||
ProtectedBranches
|
||||
.leftJoin(ProtectedBranchContexts)
|
||||
.on{ case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) }
|
||||
.map{ case (pb, c) => pb -> c.context.? }
|
||||
.filter(_._1.byPrimaryKey(owner, repository, branch))
|
||||
.list
|
||||
.groupBy(_._1)
|
||||
.map(p => p._1 -> p._2.flatMap(_._2))
|
||||
.map{ case (t1, contexts) =>
|
||||
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
|
||||
}.headOption
|
||||
|
||||
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo =
|
||||
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
|
||||
|
||||
def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] =
|
||||
ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list
|
||||
|
||||
def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, contexts: Seq[String])
|
||||
(implicit session: Session): Unit = {
|
||||
disableBranchProtection(owner, repository, branch)
|
||||
ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty))
|
||||
contexts.map{ context =>
|
||||
ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context))
|
||||
}
|
||||
}
|
||||
|
||||
def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit =
|
||||
ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete
|
||||
|
||||
}
|
||||
|
||||
object ProtectedBranchService {
|
||||
|
||||
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
|
||||
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
|
||||
(implicit session: Session): Option[String] = {
|
||||
val branch = command.getRefName.stripPrefix("refs/heads/")
|
||||
if(branch != command.getRefName){
|
||||
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case class ProtectedBranchInfo(
|
||||
owner: String,
|
||||
repository: String,
|
||||
enabled: Boolean,
|
||||
/**
|
||||
* Require status checks to pass before merging
|
||||
* Choose which status checks must pass before branches can be merged into test.
|
||||
* When enabled, commits must first be pushed to another branch,
|
||||
* then merged or pushed directly to test after status checks have passed.
|
||||
*/
|
||||
contexts: Seq[String],
|
||||
/**
|
||||
* Include administrators
|
||||
* Enforce required status checks for repository administrators.
|
||||
*/
|
||||
includeAdministrators: Boolean) extends AccountService with CommitStatusService {
|
||||
|
||||
def isAdministrator(pusher: String)(implicit session: Session): Boolean =
|
||||
pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty
|
||||
|
||||
/**
|
||||
* Can't be force pushed
|
||||
* Can't be deleted
|
||||
* Can't have changes merged into them until required status checks pass
|
||||
*/
|
||||
def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
|
||||
if(enabled){
|
||||
command.getType() match {
|
||||
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
|
||||
Some("Cannot force-push to a protected branch")
|
||||
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
|
||||
unSuccessedContexts(command.getNewId.name) match {
|
||||
case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
|
||||
case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
|
||||
case _ => None
|
||||
}
|
||||
case ReceiveCommand.Type.DELETE =>
|
||||
Some("Cannot delete a protected branch")
|
||||
case _ => None
|
||||
}
|
||||
}else{
|
||||
None
|
||||
}
|
||||
}
|
||||
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(contexts.isEmpty){
|
||||
Set.empty
|
||||
} else {
|
||||
contexts.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet
|
||||
}
|
||||
def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match {
|
||||
case _ if !enabled => false
|
||||
case _ if contexts.isEmpty => false
|
||||
case _ if includeAdministrators => true
|
||||
case p if isAdministrator(p) => false
|
||||
case _ => true
|
||||
}
|
||||
}
|
||||
object ProtectedBranchInfo{
|
||||
def disabled(owner: String, repository: String): ProtectedBranchInfo = ProtectedBranchInfo(owner, repository, false, Nil, false)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook}
|
||||
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook, CommitStatus, CommitState}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.util.JGitUtil
|
||||
import profile.simple._
|
||||
@@ -145,4 +145,29 @@ object PullRequestService {
|
||||
|
||||
case class PullRequestCount(userName: String, count: Int)
|
||||
|
||||
case class MergeStatus(
|
||||
hasConflict: Boolean,
|
||||
commitStatues:List[CommitStatus],
|
||||
branchProtection: ProtectedBranchService.ProtectedBranchInfo,
|
||||
branchIsOutOfDate: Boolean,
|
||||
hasUpdatePermission: Boolean,
|
||||
needStatusCheck: Boolean,
|
||||
hasMergePermission: Boolean,
|
||||
commitIdTo: String){
|
||||
|
||||
val statuses: List[CommitStatus] =
|
||||
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
|
||||
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
|
||||
val hasProblem = hasRequiredStatusProblem || hasConflict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
|
||||
val canUpdate = branchIsOutOfDate && !hasConflict
|
||||
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
|
||||
lazy val commitStateSummary:(CommitState, String) = {
|
||||
val stateMap = statuses.groupBy(_.state)
|
||||
val state = CommitState.combine(stateMap.keySet)
|
||||
val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")
|
||||
state -> summary
|
||||
}
|
||||
lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.exists(_==s.context) }
|
||||
lazy val isAllSuccess = commitStateSummary._1==CommitState.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user