mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-05 04:56:02 +01:00
Compare commits
657 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
abfc6eea1e | ||
|
|
fd2f3e252c | ||
|
|
3b6d0065a2 | ||
|
|
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 | ||
|
|
c73e89ccd4 | ||
|
|
f58a506780 | ||
|
|
411d19e74e | ||
|
|
bd51ffd9d2 | ||
|
|
83980fdccd | ||
|
|
baab243bc8 | ||
|
|
4dd8e1dc63 | ||
|
|
0b11b8b084 | ||
|
|
704775dc60 | ||
|
|
c7a7be1de0 | ||
|
|
e21a970977 | ||
|
|
a526dcf2dd | ||
|
|
a7b48d63e4 | ||
|
|
3677906e95 | ||
|
|
e86710fbbd | ||
|
|
73e850493a | ||
|
|
7d735f6f8a | ||
|
|
043e99a9eb | ||
|
|
65e079b1d3 | ||
|
|
32e6f584d8 | ||
|
|
ebc5219ce6 | ||
|
|
ad8e620bdf | ||
|
|
8590c693b9 | ||
|
|
5568acc5f3 | ||
|
|
c467594199 | ||
|
|
8b29bf7d93 | ||
|
|
43b7f83082 | ||
|
|
9ef5c981ef | ||
|
|
f027ac34d4 | ||
|
|
2ea31d6869 | ||
|
|
a1af7d0f9c | ||
|
|
797bf37bfa | ||
|
|
7f78815c11 | ||
|
|
d8a3f308ed | ||
|
|
74e18a982d | ||
|
|
e86ad423c5 | ||
|
|
2f00060c57 | ||
|
|
3b7e6aa68e | ||
|
|
5f3d6242fd | ||
|
|
6793a86bae | ||
|
|
d99ce20529 | ||
|
|
93e7b604cd | ||
|
|
59c18056fc | ||
|
|
5c81ce9b68 | ||
|
|
c9cf62701f | ||
|
|
f65babca4b | ||
|
|
23c146fc5d | ||
|
|
e14f336142 | ||
|
|
54647be5bd | ||
|
|
9517d65646 | ||
|
|
0156e401fa | ||
|
|
d6e2bc464d | ||
|
|
5124ff593d | ||
|
|
727c90afdc | ||
|
|
9ebd5e3265 | ||
|
|
d120142127 | ||
|
|
f9e4cddcaf | ||
|
|
8f64f174d9 | ||
|
|
d6817796b3 | ||
|
|
ab19d473c4 | ||
|
|
48f0116358 | ||
|
|
d8e6e97845 | ||
|
|
9a8920788c | ||
|
|
27864a3a3c | ||
|
|
b39e863591 | ||
|
|
d8d18ed25c | ||
|
|
7661e8cadd | ||
|
|
7d3c7a0c61 | ||
|
|
7375ff9f97 | ||
|
|
5df6ec8985 | ||
|
|
83fd2648f5 | ||
|
|
8e81758941 | ||
|
|
41a6a29771 | ||
|
|
9a8479ee58 | ||
|
|
73766f11eb | ||
|
|
a22878e2c5 | ||
|
|
1a2eb9d1e7 | ||
|
|
277ace3c8e | ||
|
|
40f376dbd9 | ||
|
|
444af0935e | ||
|
|
fb15fa0e43 | ||
|
|
bcd3e14870 | ||
|
|
c18702dcea | ||
|
|
1341ef9c52 | ||
|
|
f605a8d085 | ||
|
|
e0f7a7a3c6 | ||
|
|
1d72bed442 | ||
|
|
284b8e7c16 | ||
|
|
ff9fb24094 | ||
|
|
fde4448dd0 | ||
|
|
d16ce90a3d | ||
|
|
3ed5525956 | ||
|
|
855d1e12aa | ||
|
|
e03797a58f | ||
|
|
f0d38cf8ec | ||
|
|
2723580e17 | ||
|
|
1977aa481d | ||
|
|
4b36a8f831 | ||
|
|
96b56e38ba | ||
|
|
849d117ad3 | ||
|
|
8d57fca779 | ||
|
|
0dc867306b | ||
|
|
eefb4c01ec | ||
|
|
ccce499f7f | ||
|
|
9f11eaa4d3 | ||
|
|
7b85c0e55f | ||
|
|
7e92f1abd5 | ||
|
|
825f2518e9 | ||
|
|
def1e877db | ||
|
|
6acbd5b2cf | ||
|
|
73b7aef4a9 | ||
|
|
3d73e3922b | ||
|
|
224e44151f | ||
|
|
d9c1293985 | ||
|
|
849a40d4b5 | ||
|
|
177387e9b0 | ||
|
|
cacce54714 | ||
|
|
12082322ee | ||
|
|
7e0a5b7fec | ||
|
|
d2cf4afc81 | ||
|
|
3e0a50926f | ||
|
|
cce62de075 | ||
|
|
dff816324d | ||
|
|
d9450df7e9 | ||
|
|
41fc81fab6 | ||
|
|
aa35498bdd | ||
|
|
a33e2c6e36 | ||
|
|
75ef82d18a | ||
|
|
eb3c522122 | ||
|
|
6c8bcfc62e | ||
|
|
14becd0bd6 | ||
|
|
7390e21934 | ||
|
|
e408eb43bb | ||
|
|
dc0aa0851e | ||
|
|
51d7c43489 | ||
|
|
6b37967162 | ||
|
|
a03b9584ee | ||
|
|
34649dfeda | ||
|
|
bc84cfc2c8 | ||
|
|
bcba1f068b | ||
|
|
e6f30ef86b | ||
|
|
9e67999ef0 | ||
|
|
be75cef752 | ||
|
|
19ead71b48 | ||
|
|
7ebe1d6c62 | ||
|
|
2331b58b87 | ||
|
|
d495b04d85 | ||
|
|
751a8703ef | ||
|
|
1e6d26221d | ||
|
|
44a8e98c7b | ||
|
|
415519716e | ||
|
|
597f86dc7b | ||
|
|
579ed19949 | ||
|
|
9221bfa045 | ||
|
|
3057a31a6c | ||
|
|
d47ccf587c | ||
|
|
3e78d423ac | ||
|
|
0299cee5ec | ||
|
|
97ceffe689 | ||
|
|
9019d93449 | ||
|
|
32006e02c0 | ||
|
|
5ba0f6d51e | ||
|
|
c004d501f6 | ||
|
|
6067fa0fca | ||
|
|
e2c6658e59 | ||
|
|
0a6e50cbbe | ||
|
|
3732963d4b | ||
|
|
83bcbef6ce | ||
|
|
47cb4d1c49 | ||
|
|
32799cead7 | ||
|
|
dbedc2166b | ||
|
|
e86b404ca2 | ||
|
|
321a3a72f0 | ||
|
|
b512e7c390 | ||
|
|
ae7ead6272 | ||
|
|
db55719a6f | ||
|
|
4e1094e75b | ||
|
|
3fd97662f5 | ||
|
|
d6946b93c3 | ||
|
|
20217058fe | ||
|
|
e67365a19f | ||
|
|
c2f1817c6a | ||
|
|
4a948d9b01 | ||
|
|
377bc2703b | ||
|
|
196890b26f | ||
|
|
491fc2c164 | ||
|
|
1bed38f175 | ||
|
|
b124c31f65 | ||
|
|
8c588cbd66 | ||
|
|
334753b1ad | ||
|
|
f6e7401d1b | ||
|
|
ab564cc2d4 | ||
|
|
b10839a5c2 | ||
|
|
bb3323fb0e | ||
|
|
0b15ecbacd | ||
|
|
9f6afaed07 | ||
|
|
925420734e | ||
|
|
fb6bb12c52 | ||
|
|
22d12d0488 | ||
|
|
6888d959e1 | ||
|
|
65e66f52f6 | ||
|
|
225abfa126 | ||
|
|
876757f2d4 | ||
|
|
7e77398645 | ||
|
|
73c76a5a88 | ||
|
|
e5fca0d6cc | ||
|
|
eed7e5177f | ||
|
|
dd54ab31cb | ||
|
|
a417c373f1 | ||
|
|
5f5cc8d454 | ||
|
|
0acbaeae86 | ||
|
|
140f1eb31b | ||
|
|
172af307a6 | ||
|
|
70c386a934 | ||
|
|
08eb21844a | ||
|
|
7b37d6b571 | ||
|
|
f52bd2bcc0 |
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.2
|
||||
sudo: false
|
||||
script:
|
||||
- sbt test
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
7
CONTRIBUTING.md
Normal file
7
CONTRIBUTING.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Guideline for Issues
|
||||
|
||||
- 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.
|
||||
145
README.md
145
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
|
||||
--------
|
||||
@@ -14,29 +13,22 @@ The current version of GitBucket provides a basic features below:
|
||||
- Wiki
|
||||
- Issues
|
||||
- Fork / Pull request
|
||||
- Mail notification
|
||||
- Email notification
|
||||
- Activity timeline
|
||||
- User management (for Administrators)
|
||||
- Group (like Organization in Github)
|
||||
- LDAP integration
|
||||
- Simple user and group management with LDAP integration
|
||||
- Gravatar support
|
||||
- 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 the documentation for developers at [Wiki](https://github.com/gitbucket/gitbucket/wiki).
|
||||
|
||||
Installation
|
||||
--------
|
||||
|
||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
|
||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/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)
|
||||
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nginx)
|
||||
|
||||
The default administrator account is **root** and password is **root**.
|
||||
|
||||
@@ -47,38 +39,129 @@ 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)
|
||||
For Installation on Windows Server with IIS see [this wiki page](https://github.com/gitbucket/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
|
||||
|
||||
### Mac OS X
|
||||
#### Installing Via Homebrew
|
||||
|
||||
$ brew install gitbucket
|
||||
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
|
||||
######################################################################## 100.0%
|
||||
==> Caveats
|
||||
Note: When using launchctl the port will be 8080.
|
||||
```
|
||||
$ brew install gitbucket
|
||||
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
|
||||
######################################################################## 100.0%
|
||||
==> Caveats
|
||||
Note: When using launchctl the port will be 8080.
|
||||
|
||||
To have launchd start gitbucket at login:
|
||||
To have launchd start gitbucket at login:
|
||||
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
|
||||
Then to load gitbucket now:
|
||||
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:
|
||||
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
|
||||
==> Summary
|
||||
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
|
||||
```
|
||||
|
||||
#### Manual Installation
|
||||
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
|
||||
On OS X, generate `gitbucket.plist` by [this script](https://raw.githubusercontent.com/gitbucket/gitbucket/master/contrib/macosx/makePlist) and copy it to `~/Library/LaunchAgents/`
|
||||
|
||||
Run the following commands in `Terminal` to
|
||||
|
||||
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
|
||||
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
|
||||
|
||||
Plug-ins
|
||||
--------
|
||||
GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. Some plug-ins are available now:
|
||||
|
||||
- [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)
|
||||
|
||||
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
|
||||
|
||||
Support
|
||||
--------
|
||||
|
||||
- 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.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-pligin
|
||||
- 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
|
||||
- Limit of activity log
|
||||
|
||||
### 3.1.1 - 4 Apr 2015
|
||||
- Rolled back H2 version to avoid version compatibility issue
|
||||
- Plug-ins became possible to access ServletContext
|
||||
|
||||
### 3.1 - 28 Mar 2015
|
||||
- Web APIs for Jenkins github pull-request builder
|
||||
- Improved diff view
|
||||
- Bump Scalatra to 2.3.1, sbt to 0.13.8
|
||||
|
||||
### 3.0 - 3 Mar 2015
|
||||
- New plug-in system is available
|
||||
- Connection pooling by c3p0
|
||||
- New branch UI
|
||||
- Compare between specified commit ids
|
||||
|
||||
### 2.8 - 1 Feb 2015
|
||||
- New logo and icons
|
||||
- New system setting options to control visibility
|
||||
@@ -259,3 +342,7 @@ Release Notes
|
||||
|
||||
### 1.0 - 04 Jul 2013
|
||||
- This is a first public release
|
||||
|
||||
Sponsors
|
||||
--------
|
||||
[](https://www.jetbrains.com/idea/)
|
||||
|
||||
@@ -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
|
||||
|
||||
22
doc/activity.md
Normal file
22
doc/activity.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Activity Timeline
|
||||
========
|
||||
GitBucket records several types of user activity to ```ACTIVITY``` table. Activity types are shown below:
|
||||
|
||||
type | message | additional information
|
||||
------------------|------------------------------------------------------|------------------------
|
||||
create_repository |$user created $owner/$repo |-
|
||||
open_issue |$user opened issue $owner/$repo#$issueId |-
|
||||
close_issue |$user closed issue $owner/$repo#$issueId |-
|
||||
close_issue |$user closed pull request $owner/$repo#$issueId |-
|
||||
reopen_issue |$user reopened issue $owner/$repo#$issueId |-
|
||||
comment_issue |$user commented on issue $owner/$repo#$issueId |-
|
||||
comment_issue |$user commented on pull request $owner/$repo#$issueId |-
|
||||
create_wiki |$user created the $owner/$repo wiki |$page
|
||||
edit_wiki |$user edited the $owner/$repo wiki |$page<br>$page:$commitId(since 1.5)
|
||||
push |$user pushed to $owner/$repo#$branch to $owner/$repo |$commitId:$shortMessage\n*
|
||||
create_tag |$user created tag $tag at $owner/$repo |-
|
||||
create_branch |$user created branch $branch at $owner/$repo |-
|
||||
delete_branch |$user deleted branch $branch at $owner/$repo |-
|
||||
fork |$user forked $owner/$repo to $owner/$repo |-
|
||||
open_pullreq |$user opened pull request $owner/$repo#issueId |-
|
||||
merge_pullreq |$user merge pull request $owner/$repo#issueId |-
|
||||
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.
|
||||
37
doc/auto_update.md
Normal file
37
doc/auto_update.md
Normal file
@@ -0,0 +1,37 @@
|
||||
Automatic Schema Updating
|
||||
========
|
||||
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading.
|
||||
|
||||
To release a new version of GitBucket, add the version definition to the [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.
|
||||
*/
|
||||
val versions = Seq(
|
||||
Version(1, 0)
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```scala
|
||||
val versions = Seq(
|
||||
new Version(1, 3){
|
||||
override def update(conn: Connection): Unit = {
|
||||
super.update(conn)
|
||||
// Add any code here!
|
||||
}
|
||||
},
|
||||
Version(1, 2),
|
||||
Version(1, 1),
|
||||
Version(1, 0)
|
||||
)
|
||||
```
|
||||
56
doc/comment_action.md
Normal file
56
doc/comment_action.md
Normal file
@@ -0,0 +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.
|
||||
|
||||
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 |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
|
||||
|
||||
This value is saved when users have made a normal comment.
|
||||
|
||||
### close_comment, reopen_comment
|
||||
|
||||
These values are saved when users have reopened or closed the issue with comments.
|
||||
|
||||
### close, reopen
|
||||
|
||||
These values are saved when users have reopened or closed the issue.
|
||||
At the same time, store the fixed value(i.e. "Close" or "Reopen") to the `CONTENT` column.
|
||||
Therefore, this comment is not displayed, and not counted as a comment.
|
||||
|
||||
### commit
|
||||
|
||||
This value is saved when users have pushed including the `#issueId` to the commit message.
|
||||
At the same time, store it to the `CONTENT` column with its commit id.
|
||||
This comment is displayed. But it can not be edited by all users, and also not counted as a comment.
|
||||
|
||||
### merge
|
||||
|
||||
This value is saved when users have merged the pull request.
|
||||
At the same time, store the message to the `CONTENT` column.
|
||||
This comment is displayed. But it can not be edited by all users, and also not counted as a comment.
|
||||
|
||||
### delete_branch
|
||||
|
||||
This value is saved when users have deleted the branch. Users can delete branch after merging pull request which is requested from the same repository.
|
||||
At the same time, store it to the `CONTENT` column with the deleted branch name.
|
||||
Therefore, this comment is not displayed, and not counted as a comment.
|
||||
|
||||
### refer
|
||||
|
||||
This value is saved when other issue or issue comment contains reference to the issue like `#issueId`.
|
||||
At the same time, store id and title of the referrer issue as `id:title`.
|
||||
44
doc/directory.md
Normal file
44
doc/directory.md
Normal file
@@ -0,0 +1,44 @@
|
||||
Directory Structure
|
||||
========
|
||||
GitBucket persists all data into __HOME/.gitbucket__ in default (In 1.9 or before, HOME/gitbucket is default).
|
||||
|
||||
This directory has following structure:
|
||||
|
||||
```
|
||||
* /HOME/gitbucket
|
||||
* /repositories
|
||||
* /USER_NAME
|
||||
* / REPO_NAME.git (substance of repository. GitServlet sees this directory)
|
||||
* / REPO_NAME
|
||||
* /issues (files which are attached to issue)
|
||||
* / REPO_NAME.wiki.git (wiki repository)
|
||||
* /data
|
||||
* /USER_NAME
|
||||
* /files
|
||||
* avatar.xxx (image file of user avatar)
|
||||
* /plugins
|
||||
* /PLUGIN_NAME
|
||||
* plugin.js
|
||||
* /tmp
|
||||
* /_upload
|
||||
* /SESSION_ID (removed at session timeout)
|
||||
* current time millis + random 10 alphanumeric chars (temporary file for file uploading)
|
||||
* /USER_NAME
|
||||
* /init-REPO_NAME (used in repository creation and removed after it) ... unused since 1.8
|
||||
* /REPO_NAME.wiki (working directory for wiki repository) ... unused since 1.8
|
||||
* /REPO_NAME
|
||||
* /download (temporary directories are created under this directory)
|
||||
```
|
||||
|
||||
There are some ways to specify the data directory instead of the default location.
|
||||
|
||||
1. Environment variable __GITBUCKET_HOME__
|
||||
2. System property __gitbucket.home__ (e.g. ```-Dgitbucket.home=PATH_TO_DATADIR```)
|
||||
3. Command line option for embedded Jetty (e.g. ```java -jar gitbucket.war --data=PATH_TO_DATADIR```)
|
||||
4. Context parameter __gitbucket.home__ in web.xml like below:
|
||||
```xml
|
||||
<context-param>
|
||||
<param-name>gitbucket.home</param-name>
|
||||
<param-value>PATH_TO_DATADIR</param-value>
|
||||
</context-param>
|
||||
```
|
||||
33
doc/how_to_run.md
Normal file
33
doc/how_to_run.md
Normal file
@@ -0,0 +1,33 @@
|
||||
How to run from the source tree
|
||||
========
|
||||
|
||||
Run for Development
|
||||
--------
|
||||
|
||||
If you want to test GitBucket, input following command at the root directory of the source tree.
|
||||
|
||||
```
|
||||
$ sbt ~jetty:start
|
||||
```
|
||||
|
||||
Then access to `http://localhost:8080/` by your browser. The default administrator account is `root` and password is `root`.
|
||||
|
||||
Source code modification is detected and reloaded automatically. You can modify logging configuration by editing `src/main/resources/logback-dev.xml`.
|
||||
|
||||
Build war file
|
||||
--------
|
||||
|
||||
To build war file, run the following command:
|
||||
|
||||
```
|
||||
$ sbt package
|
||||
```
|
||||
|
||||
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
|
||||
|
||||
To build executable war file, run
|
||||
|
||||
* Windows: Not available
|
||||
* Linux: `./release/make-release-war.sh`
|
||||
|
||||
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.
|
||||
@@ -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 |
23
doc/notification.md
Normal file
23
doc/notification.md
Normal file
@@ -0,0 +1,23 @@
|
||||
Notification Email
|
||||
========
|
||||
|
||||
GitBucket sends email to target users by enabling the notification email by an administrator.
|
||||
|
||||
The timing of the notification are as follows:
|
||||
|
||||
##### at the issue registration (new issue, new pull request)
|
||||
When a record is saved into the ```ISSUE``` table, GitBucket does the notification.
|
||||
|
||||
##### at the comment registration
|
||||
Among the records in the ```ISSUE_COMMENT``` table, them to be counted as a comment (i.e. the record ```ACTION``` column value is "comment" or "close_comment" or "reopen_comment") are saved, GitBucket does the notification.
|
||||
|
||||
##### at the status update (close, reopen, merge)
|
||||
When the ```CLOSED``` column value is updated, GitBucket does the notification.
|
||||
|
||||
Notified users are as follows:
|
||||
|
||||
* individual repository's owner
|
||||
* collaborators
|
||||
* participants
|
||||
|
||||
However, the operation in person is excluded from the target.
|
||||
11
doc/readme.md
Normal file
11
doc/readme.md
Normal file
@@ -0,0 +1,11 @@
|
||||
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](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)
|
||||
54
doc/release.md
Normal file
54
doc/release.md
Normal file
@@ -0,0 +1,54 @@
|
||||
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 BitBucket 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 `release/make-release-war.sh`. The release war file is generated into `target/scala-2.11/gitbucket.war`.
|
||||
|
||||
```bash
|
||||
$ cd release
|
||||
$ ./make-release-war.sh
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
71
doc/validation.md
Normal file
71
doc/validation.md
Normal file
@@ -0,0 +1,71 @@
|
||||
Mapping and Validation
|
||||
========
|
||||
GitBucket uses [scalatra-forms](https://github.com/takezoe/scalatra-forms) to validate request parameters and map them to the scala object. This is inspired by Play2 form mapping / validation.
|
||||
|
||||
At first, define the mapping as following:
|
||||
|
||||
```scala
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
case class RegisterForm(name: String, description: String)
|
||||
|
||||
val form = mapping(
|
||||
"name" -> text(required, maxlength(40)),
|
||||
"description" -> text()
|
||||
)(RegisterForm.apply)
|
||||
```
|
||||
|
||||
The servlet have to mixed in ```jp.sf.amateras.scalatra.forms.ClientSideValidationFormSupport``` to validate request parameters and take mapped object. It validates request parameters before action. If any errors are detected, it throws an exception.
|
||||
|
||||
```scala
|
||||
class RegisterServlet extends ScalatraServlet with ClientSideValidationFormSupport {
|
||||
post("/register", form) { form: RegisterForm =>
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the view template, you can add client-side validation by adding ```validate="true"``` to your form. Error messages are set to ```span#error-<fieldname>```.
|
||||
|
||||
```html
|
||||
<form method="POST" action="/register" validate="true">
|
||||
Name: <input type="name" type="text">
|
||||
<span class="error" id="error-name"></span>
|
||||
<br/>
|
||||
Description: <input type="description" type="text">
|
||||
<span class="error" id="error-description"></span>
|
||||
<br/>
|
||||
<input type="submit" value="Register"/>
|
||||
</form>
|
||||
```
|
||||
|
||||
Client-side validation calls ```<form-action>/validate``` to validate form contents. It returns a validation result as JSON. In this case, form action is ```/register```, so ```/register/validate``` is called before submitting a form. ```ClientSideValidationFormSupport``` adds this JSON API automatically.
|
||||
|
||||
For Ajax request, you have to use '''ajaxGet''' or '''ajaxPost''' to define action. It almost same as '''get''' or '''post'''. You can implement actions which handle Ajax request as same as normal actions.
|
||||
Small difference is they return validation errors as JSON.
|
||||
|
||||
```scala
|
||||
ajaxPost("/register", form){ form =>
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
You can call these actions using jQuery as below:
|
||||
|
||||
```javascript
|
||||
$('#register').click(function(e){
|
||||
$.ajax($(this).attr('action'), {
|
||||
type: 'POST',
|
||||
data: {
|
||||
name: $('#name').val(),
|
||||
mail: $('#mail').val()
|
||||
}
|
||||
})
|
||||
.done(function(data){
|
||||
$('#result').text('Registered!');
|
||||
})
|
||||
.fail(function(data, status){
|
||||
displayErrors($.parseJSON(data.responseText));
|
||||
});
|
||||
});
|
||||
```
|
||||
Binary file not shown.
BIN
embed-jetty/jetty-http-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-http-8.1.16.v20140903.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
embed-jetty/jetty-io-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-io-8.1.16.v20140903.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
embed-jetty/jetty-server-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-server-8.1.16.v20140903.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
embed-jetty/jetty-servlet-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-servlet-8.1.16.v20140903.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
embed-jetty/jetty-util-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-util-8.1.16.v20140903.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
embed-jetty/jetty-webapp-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-webapp-8.1.16.v20140903.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
embed-jetty/jetty-xml-8.1.16.v20140903.jar
Normal file
BIN
embed-jetty/jetty-xml-8.1.16.v20140903.jar
Normal file
Binary file not shown.
Binary file not shown.
11
embed-jetty/update.sh
Executable file
11
embed-jetty/update.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
version=$1
|
||||
output_dir=`dirname $0`
|
||||
git rm -f ${output_dir}/jetty-*.jar
|
||||
for name in 'io' 'servlet' 'xml' 'continuation' 'security' 'util' 'http' 'server' 'webapp'
|
||||
do
|
||||
jar_filename="jetty-${name}-${version}.jar"
|
||||
wget "http://repo1.maven.org/maven2/org/eclipse/jetty/jetty-${name}/${version}/${jar_filename}" -O ${output_dir}/${jar_filename}
|
||||
done
|
||||
git add ${output_dir}/*.jar
|
||||
git commit
|
||||
@@ -1 +1 @@
|
||||
sbt.version=0.13.5
|
||||
sbt.version=0.13.8
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import sbt._
|
||||
import Keys._
|
||||
import org.scalatra.sbt._
|
||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
||||
import com.earldouglas.xwp.JettyPlugin
|
||||
import play.twirl.sbt.SbtTwirl
|
||||
import play.twirl.sbt.Import.TwirlKeys._
|
||||
import sbt.Keys._
|
||||
import sbt._
|
||||
import sbtassembly.AssemblyKeys._
|
||||
import sbtassembly._
|
||||
import JettyPlugin.autoImport._
|
||||
|
||||
object MyBuild extends Build {
|
||||
val Organization = "jp.sf.amateras"
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "0.0.1"
|
||||
val ScalaVersion = "2.11.2"
|
||||
val ScalatraVersion = "2.3.0"
|
||||
val Version = "3.10.0"
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
|
||||
lazy val project = Project (
|
||||
"gitbucket",
|
||||
file(".")
|
||||
)
|
||||
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
|
||||
// .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,
|
||||
@@ -25,35 +37,44 @@ object MyBuild extends Build {
|
||||
scalaVersion := ScalaVersion,
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
|
||||
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
||||
),
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.2.201412180340-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.2.201412180340-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.2.10",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
|
||||
"org.json4s" %% "json4s-jackson" % "3.2.11",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.2.0",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"org.pegdown" % "pegdown" % "1.4.1",
|
||||
"org.apache.commons" % "commons-compress" % "1.5",
|
||||
"org.apache.commons" % "commons-email" % "1.3.1",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.3",
|
||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.6-SNAPSHOT",
|
||||
"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" % "1.0.0",
|
||||
"org.apache.tika" % "tika-core" % "1.10",
|
||||
"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.8.v20121106" % "container;provided",
|
||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||
"junit" % "junit" % "4.11" % "test",
|
||||
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
|
||||
"com.h2database" % "h2" % "1.4.190",
|
||||
"ch.qos.logback" % "logback-classic" % "1.1.1",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided",
|
||||
"junit" % "junit" % "4.12" % "test",
|
||||
"com.mchange" % "c3p0" % "0.9.5.2",
|
||||
"com.typesafe" % "config" % "1.2.1",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x" exclude("c3p0","c3p0")
|
||||
),
|
||||
EclipseKeys.withSource := true,
|
||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
|
||||
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
|
||||
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")
|
||||
).enablePlugins(SbtTwirl)
|
||||
).enablePlugins(SbtTwirl, JettyPlugin)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
|
||||
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||
|
||||
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.2")
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
|
||||
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")
|
||||
@@ -1,12 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<project name="gitbucket" default="all" basedir=".">
|
||||
<project name="gitbucket" default="all" basedir="..">
|
||||
|
||||
<property environment="env"/>
|
||||
<property name="target.dir" value="target"/>
|
||||
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
|
||||
<property name="jetty.dir" value="embed-jetty"/>
|
||||
<property name="scala.version" value="2.11"/>
|
||||
<property name="gitbucket.version" value="0.0.1"/>
|
||||
<property name="jetty.version" value="8.1.8.v20121106"/>
|
||||
<property name="gitbucket.version" value="${env.GITBUCKET_VERSION}"/>
|
||||
<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">
|
||||
@@ -54,7 +55,12 @@
|
||||
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
|
||||
</target>
|
||||
|
||||
<target name="all" depends="rename">
|
||||
<target name="checksum" depends="rename">
|
||||
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="MD5" format="MD5SUM" forceOverwrite="yes" fileext=".md5"/>
|
||||
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="SHA" format="MD5SUM" forceOverwrite="yes" fileext=".sha1"/>
|
||||
</target>
|
||||
|
||||
<target name="all" depends="checksum">
|
||||
</target>
|
||||
|
||||
|
||||
15
release/deploy-assembly-jar.sh
Executable file
15
release/deploy-assembly-jar.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
. ./env.sh
|
||||
|
||||
cd ../
|
||||
./sbt.sh clean assembly
|
||||
|
||||
cd release
|
||||
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/
|
||||
3
release/env.sh
Normal file
3
release/env.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
export GITBUCKET_VERSION=`cat ../project/build.scala | grep 'val Version' | cut -d \" -f 2`
|
||||
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"
|
||||
15
release/make-release-war.sh
Executable file
15
release/make-release-war.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
D="$(dirname "$0")"
|
||||
D="$(cd "${D}"; pwd)"
|
||||
DD="$(dirname "${D}")"
|
||||
(
|
||||
for f in "${D}/env.sh" "${D}/build.xml"; do
|
||||
if [ ! -s "${f}" ]; then
|
||||
echo >&2 "$0: Unable to access file '${f}'"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
. "${D}/env.sh"
|
||||
cd "${DD}"
|
||||
ant -f "${D}/build.xml" all
|
||||
)
|
||||
17
release/pom.xml
Normal file
17
release/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>jp.sf.amateras</groupId>
|
||||
<artifactId>gitbucket-assembly</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>org.apache.maven.wagon</groupId>
|
||||
<artifactId>wagon-ssh</artifactId>
|
||||
<version>1.0-beta-6</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
</build>
|
||||
</project>
|
||||
Binary file not shown.
BIN
sbt-launch-0.13.8.jar
Normal file
BIN
sbt-launch-0.13.8.jar
Normal file
Binary file not shown.
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
||||
set SCRIPT_DIR=%~dp0
|
||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.5.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.8.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.5.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.8.jar "$@"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package util;
|
||||
package gitbucket.core.util;
|
||||
|
||||
import org.eclipse.jgit.api.errors.PatchApplyException;
|
||||
import org.eclipse.jgit.diff.RawText;
|
||||
6
src/main/resources/database.conf
Normal file
6
src/main/resources/database.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
db {
|
||||
driver = "org.h2.Driver"
|
||||
url = "jdbc:h2:${DatabaseHome};MVCC=true"
|
||||
user = "sa"
|
||||
password = "sa"
|
||||
}
|
||||
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,6 +6,13 @@
|
||||
</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>
|
||||
@@ -13,5 +20,7 @@
|
||||
<!--
|
||||
<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
|
||||
|
||||
42
src/main/resources/update/3_1.sql
Normal file
42
src/main/resources/update/3_1.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
DROP TABLE IF EXISTS ACCESS_TOKEN;
|
||||
|
||||
CREATE TABLE ACCESS_TOKEN (
|
||||
ACCESS_TOKEN_ID INT NOT NULL AUTO_INCREMENT,
|
||||
TOKEN_HASH VARCHAR(40) NOT NULL,
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
NOTE TEXT NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_PK PRIMARY KEY (ACCESS_TOKEN_ID);
|
||||
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH);
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS COMMIT_STATUS;
|
||||
CREATE TABLE COMMIT_STATUS(
|
||||
COMMIT_STATUS_ID INT AUTO_INCREMENT,
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
COMMIT_ID VARCHAR(40) NOT NULL,
|
||||
CONTEXT VARCHAR(255) NOT NULL, -- context is too long (maximum is 255 characters)
|
||||
STATE VARCHAR(10) NOT NULL, -- pending, success, error, or failure
|
||||
TARGET_URL VARCHAR(200),
|
||||
DESCRIPTION TEXT,
|
||||
CREATOR VARCHAR(100) NOT NULL,
|
||||
REGISTERED_DATE TIMESTAMP NOT NULL, -- CREATED_AT
|
||||
UPDATED_DATE TIMESTAMP NOT NULL -- UPDATED_AT
|
||||
);
|
||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_PK PRIMARY KEY (COMMIT_STATUS_ID);
|
||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_1
|
||||
UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT);
|
||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK1
|
||||
FOREIGN KEY (USER_NAME, REPOSITORY_NAME)
|
||||
REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK2
|
||||
FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK3
|
||||
FOREIGN KEY (CREATOR) REFERENCES ACCOUNT (USER_NAME)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
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;
|
||||
@@ -1,9 +1,14 @@
|
||||
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
|
||||
import app._
|
||||
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||
import org.scalatra._
|
||||
import javax.servlet._
|
||||
|
||||
import gitbucket.core.controller._
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, Database, TransactionFilter}
|
||||
import gitbucket.core.util.Directory
|
||||
|
||||
import java.util.EnumSet
|
||||
import javax.servlet._
|
||||
|
||||
import org.scalatra._
|
||||
|
||||
|
||||
class ScalatraBootstrap extends LifeCycle {
|
||||
override def init(context: ServletContext) {
|
||||
@@ -12,15 +17,22 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
||||
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
||||
|
||||
context.addFilter("accessTokenAuthenticationFilter", new AccessTokenAuthenticationFilter)
|
||||
context.getFilterRegistration("accessTokenAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
|
||||
// Register controllers
|
||||
context.mount(new AnonymousAccessController, "/*")
|
||||
|
||||
PluginRegistry().getControllers.foreach { case (controller, path) =>
|
||||
context.mount(controller, path)
|
||||
}
|
||||
|
||||
context.mount(new IndexController, "/")
|
||||
context.mount(new SearchController, "/")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
context.mount(new DashboardController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
context.mount(new PluginsController, "/*")
|
||||
context.mount(new AccountController, "/*")
|
||||
context.mount(new RepositoryViewerController, "/*")
|
||||
context.mount(new WikiController, "/*")
|
||||
@@ -31,9 +43,13 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.mount(new RepositorySettingsController, "/*")
|
||||
|
||||
// Create GITBUCKET_HOME directory if it does not exist
|
||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
||||
val dir = new java.io.File(Directory.GitBucketHome)
|
||||
if(!dir.exists){
|
||||
dir.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
override def destroy(context: ServletContext): Unit = {
|
||||
Database.closeDataSource()
|
||||
}
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import service._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import scala.collection.JavaConverters._
|
||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||
import service.IssuesService._
|
||||
import service.PullRequestService._
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.eclipse.jgit.merge.MergeStrategy
|
||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import service.WebHookService.WebHookPayload
|
||||
import util.JGitUtil.DiffInfo
|
||||
import util.JGitUtil.CommitInfo
|
||||
|
||||
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||
|
||||
val pullRequestForm = mapping(
|
||||
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||
"content" -> trim(label("Content", optional(text()))),
|
||||
"targetUserName" -> trim(text(required, maxlength(100))),
|
||||
"targetBranch" -> trim(text(required, maxlength(100))),
|
||||
"requestUserName" -> trim(text(required, maxlength(100))),
|
||||
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||
"commitIdTo" -> trim(text(required, maxlength(40)))
|
||||
)(PullRequestForm.apply)
|
||||
|
||||
val mergeForm = mapping(
|
||||
"message" -> trim(label("Message", text(required)))
|
||||
)(MergeForm.apply)
|
||||
|
||||
case class PullRequestForm(
|
||||
title: String,
|
||||
content: Option[String],
|
||||
targetUserName: String,
|
||||
targetBranch: String,
|
||||
requestUserName: String,
|
||||
requestRepositoryName: String,
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String)
|
||||
|
||||
case class MergeForm(message: String)
|
||||
|
||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||
val q = request.getParameter("q")
|
||||
if(Option(q).exists(_.contains("is:issue"))){
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
|
||||
} else {
|
||||
searchPullRequests(None, repository)
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||
val (commits, diffs) =
|
||||
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
||||
|
||||
pulls.html.pullreq(
|
||||
issue, pullreq,
|
||||
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
|
||||
.sortWith((a, b) => a.registeredDate before b.registeredDate),
|
||||
getIssueLabels(owner, name, issueId),
|
||||
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||
getMilestonesWithIssueCount(owner, name),
|
||||
getLabels(owner, name),
|
||||
commits,
|
||||
diffs,
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
repository)
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
pulls.html.mergeguide(
|
||||
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
|
||||
pullreq,
|
||||
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
|
||||
params("id").toIntOpt.map { issueId =>
|
||||
val branchName = multiParams("splat").head
|
||||
val userName = context.loginAccount.get.userName
|
||||
if(repository.repository.defaultBranch != branchName){
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(branchName).call()
|
||||
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||
}
|
||||
}
|
||||
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||
params("id").toIntOpt.flatMap { issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
LockUtil.lock(s"${owner}/${name}"){
|
||||
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||
using(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// merge
|
||||
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
val conflicted = try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
if (conflicted) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
// creates merge commit
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(merger.getResultTreeId)
|
||||
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
||||
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
mergeCommit.setAuthor(personIdent)
|
||||
mergeCommit.setCommitter(personIdent)
|
||||
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" +
|
||||
form.message)
|
||||
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = git.getRepository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
|
||||
// update refs
|
||||
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(false)
|
||||
refUpdate.setRefLogIdent(personIdent)
|
||||
refUpdate.setRefLogMessage("merged", true)
|
||||
refUpdate.update()
|
||||
|
||||
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
|
||||
if(pullreq.branch == defaultBranch){
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
}
|
||||
issue.content match {
|
||||
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
|
||||
case _ =>
|
||||
}
|
||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||
}
|
||||
// call web hook
|
||||
getWebHookURLs(owner, name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(ownerAccount <- getAccountByUserName(owner)){
|
||||
callWebHook(owner, name, webHookURLs,
|
||||
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
||||
using(
|
||||
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ (oldGit, newGit) =>
|
||||
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
||||
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
||||
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
case _ => {
|
||||
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
|
||||
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
} getOrElse {
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner){
|
||||
Some(forkedRepository.name)
|
||||
} else {
|
||||
forkedRepository.repository.originRepositoryName.orElse {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||
originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||
|
||||
val oldId = oldGit.getRepository.resolve(forkedId)
|
||||
val newId = newGit.getRepository.resolve(forkedBranch)
|
||||
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner, originRepository.name, oldId.getName,
|
||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||
|
||||
pulls.html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
},
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originBranch,
|
||||
forkedBranch,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner){
|
||||
Some(forkedRepository.name)
|
||||
} else {
|
||||
forkedRepository.repository.originRepositoryName.orElse {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
pulls.html.mergecheck(
|
||||
checkConflict(originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch))
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
|
||||
val issueId = createIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginUserName,
|
||||
title = form.title,
|
||||
content = form.content,
|
||||
assignedUserName = None,
|
||||
milestoneId = None,
|
||||
isPullRequest = true)
|
||||
|
||||
createPullRequest(
|
||||
originUserName = repository.owner,
|
||||
originRepositoryName = repository.name,
|
||||
issueId = issueId,
|
||||
originBranch = form.targetBranch,
|
||||
requestUserName = form.requestUserName,
|
||||
requestRepositoryName = form.requestRepositoryName,
|
||||
requestBranch = form.requestBranch,
|
||||
commitIdFrom = form.commitIdFrom,
|
||||
commitIdTo = form.commitIdTo)
|
||||
|
||||
// fetch requested branch
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
|
||||
.call
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||
*/
|
||||
private def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||
LockUtil.lock(s"${userName}/${repositoryName}"){
|
||||
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${branch}"
|
||||
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||
try {
|
||||
// fetch objects from origin repository branch
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||
.setRefSpecs(refSpec)
|
||||
.call
|
||||
|
||||
// merge conflict check
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
|
||||
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
} finally {
|
||||
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
|
||||
*/
|
||||
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||
issueId: Int): Boolean = {
|
||||
LockUtil.lock(s"${userName}/${repositoryName}") {
|
||||
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
// merge
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
*
|
||||
* - "owner:branch" to ("owner", "branch")
|
||||
* - "branch" to ("defaultOwner", "branch")
|
||||
*/
|
||||
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
|
||||
if(value.contains(':')){
|
||||
val array = value.split(":")
|
||||
(array(0), array(1))
|
||||
} else {
|
||||
(defaultOwner, value)
|
||||
}
|
||||
|
||||
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
|
||||
using(
|
||||
Git.open(getRepositoryDir(userName, repositoryName)),
|
||||
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
|
||||
){ (oldGit, newGit) =>
|
||||
val oldId = oldGit.getRepository.resolve(branch)
|
||||
val newId = newGit.getRepository.resolve(requestCommitId)
|
||||
|
||||
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
|
||||
new CommitInfo(revCommit)
|
||||
}.toList.splitWith { (commit1, commit2) =>
|
||||
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
}
|
||||
|
||||
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
|
||||
|
||||
(commits, diffs)
|
||||
}
|
||||
|
||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
|
||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val sessionKey = Keys.Session.Pulls(owner, repoName)
|
||||
|
||||
// retrieve search condition
|
||||
val condition = session.putAndGet(sessionKey,
|
||||
if(request.hasQueryString) IssueSearchCondition(request)
|
||||
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
issues.html.list(
|
||||
"pulls",
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
|
||||
condition,
|
||||
repository,
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.{Account, CommitState, CommitStatus}
|
||||
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
||||
*/
|
||||
case class ApiCombinedCommitStatus(
|
||||
state: String,
|
||||
sha: String,
|
||||
total_count: Int,
|
||||
statuses: Iterable[ApiCommitStatus],
|
||||
repository: ApiRepository){
|
||||
// val commit_url = ApiPath(s"/api/v3/repos/${repository.full_name}/${sha}")
|
||||
val url = ApiPath(s"/api/v3/repos/${repository.full_name}/commits/${sha}/status")
|
||||
}
|
||||
object ApiCombinedCommitStatus {
|
||||
def apply(sha:String, statuses: Iterable[(CommitStatus, Account)], repository:ApiRepository): ApiCombinedCommitStatus = ApiCombinedCommitStatus(
|
||||
state = CommitState.combine(statuses.map(_._1.state).toSet).name,
|
||||
sha = sha,
|
||||
total_count= statuses.size,
|
||||
statuses = statuses.map{ case (s, a)=> ApiCommitStatus(s, ApiUser(a)) },
|
||||
repository = repository)
|
||||
}
|
||||
29
src/main/scala/gitbucket/core/api/ApiComment.scala
Normal file
29
src/main/scala/gitbucket/core/api/ApiComment.scala
Normal file
@@ -0,0 +1,29 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.IssueComment
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import java.util.Date
|
||||
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/comments/
|
||||
*/
|
||||
case class ApiComment(
|
||||
id: Int,
|
||||
user: ApiUser,
|
||||
body: String,
|
||||
created_at: Date,
|
||||
updated_at: Date)(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, 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)(repositoryName, issueId, isPullRequest)
|
||||
}
|
||||
57
src/main/scala/gitbucket/core/api/ApiCommit.scala
Normal file
57
src/main/scala/gitbucket/core/api/ApiCommit.scala
Normal file
@@ -0,0 +1,57 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.util.JGitUtil
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/commits/
|
||||
*/
|
||||
case class ApiCommit(
|
||||
id: String,
|
||||
message: String,
|
||||
timestamp: Date,
|
||||
added: List[String],
|
||||
removed: List[String],
|
||||
modified: List[String],
|
||||
author: ApiPersonIdent,
|
||||
committer: ApiPersonIdent)(repositoryName:RepositoryName, 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, urlIsHtmlUrl: Boolean = false): ApiCommit = {
|
||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||
ApiCommit(
|
||||
id = commit.id,
|
||||
message = commit.fullMessage,
|
||||
timestamp = commit.commitTime,
|
||||
added = diffs._1.collect {
|
||||
case x if x.changeType == DiffEntry.ChangeType.ADD => x.newPath
|
||||
},
|
||||
removed = diffs._1.collect {
|
||||
case x if x.changeType == DiffEntry.ChangeType.DELETE => x.oldPath
|
||||
},
|
||||
modified = diffs._1.collect {
|
||||
case x if x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE => x.newPath
|
||||
},
|
||||
author = ApiPersonIdent.author(commit),
|
||||
committer = ApiPersonIdent.committer(commit)
|
||||
)(repositoryName, urlIsHtmlUrl)
|
||||
}
|
||||
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
|
||||
}
|
||||
42
src/main/scala/gitbucket/core/api/ApiCommitListItem.scala
Normal file
42
src/main/scala/gitbucket/core/api/ApiCommitListItem.scala
Normal file
@@ -0,0 +1,42 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.api.ApiCommitListItem._
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/commits/
|
||||
*/
|
||||
case class ApiCommitListItem(
|
||||
sha: String,
|
||||
commit: Commit,
|
||||
author: Option[ApiUser],
|
||||
committer: Option[ApiUser],
|
||||
parents: Seq[Parent])(repositoryName: RepositoryName) {
|
||||
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
|
||||
}
|
||||
|
||||
object ApiCommitListItem {
|
||||
def apply(commit: CommitInfo, repositoryName: RepositoryName): ApiCommitListItem = ApiCommitListItem(
|
||||
sha = commit.id,
|
||||
commit = Commit(
|
||||
message = commit.fullMessage,
|
||||
author = ApiPersonIdent.author(commit),
|
||||
committer = ApiPersonIdent.committer(commit)
|
||||
)(commit.id, repositoryName),
|
||||
author = None,
|
||||
committer = None,
|
||||
parents = commit.parents.map(Parent(_)(repositoryName)))(repositoryName)
|
||||
|
||||
case class Parent(sha: String)(repositoryName: RepositoryName){
|
||||
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
|
||||
}
|
||||
|
||||
case class Commit(
|
||||
message: String,
|
||||
author: ApiPersonIdent,
|
||||
committer: ApiPersonIdent)(sha:String, repositoryName: RepositoryName) {
|
||||
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${sha}")
|
||||
}
|
||||
}
|
||||
38
src/main/scala/gitbucket/core/api/ApiCommitStatus.scala
Normal file
38
src/main/scala/gitbucket/core/api/ApiCommitStatus.scala
Normal file
@@ -0,0 +1,38 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.CommitStatus
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import java.util.Date
|
||||
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||
*/
|
||||
case class ApiCommitStatus(
|
||||
created_at: Date,
|
||||
updated_at: Date,
|
||||
state: String,
|
||||
target_url: Option[String],
|
||||
description: Option[String],
|
||||
id: Int,
|
||||
context: String,
|
||||
creator: ApiUser
|
||||
)(sha: String, repositoryName: RepositoryName) {
|
||||
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}/statuses")
|
||||
}
|
||||
|
||||
|
||||
object ApiCommitStatus {
|
||||
def apply(status: CommitStatus, creator:ApiUser): ApiCommitStatus = ApiCommitStatus(
|
||||
created_at = status.registeredDate,
|
||||
updated_at = status.updatedDate,
|
||||
state = status.state.name,
|
||||
target_url = status.targetUrl,
|
||||
description= status.description,
|
||||
id = status.commitStatusId,
|
||||
context = status.context,
|
||||
creator = creator
|
||||
)(status.commitId, RepositoryName(status))
|
||||
}
|
||||
5
src/main/scala/gitbucket/core/api/ApiError.scala
Normal file
5
src/main/scala/gitbucket/core/api/ApiError.scala
Normal file
@@ -0,0 +1,5 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
case class ApiError(
|
||||
message: String,
|
||||
documentation_url: Option[String] = None)
|
||||
35
src/main/scala/gitbucket/core/api/ApiIssue.scala
Normal file
35
src/main/scala/gitbucket/core/api/ApiIssue.scala
Normal file
@@ -0,0 +1,35 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Issue
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import java.util.Date
|
||||
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/
|
||||
*/
|
||||
case class ApiIssue(
|
||||
number: Int,
|
||||
title: String,
|
||||
user: ApiUser,
|
||||
// labels,
|
||||
state: String,
|
||||
created_at: Date,
|
||||
updated_at: Date,
|
||||
body: String)(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, repositoryName: RepositoryName, user: ApiUser): ApiIssue =
|
||||
ApiIssue(
|
||||
number = issue.issueId,
|
||||
title = issue.title,
|
||||
user = user,
|
||||
state = if(issue.closed){ "closed" }else{ "open" },
|
||||
body = issue.content.getOrElse(""),
|
||||
created_at = issue.registeredDate,
|
||||
updated_at = issue.updatedDate)(repositoryName, issue.isPullRequest)
|
||||
}
|
||||
6
src/main/scala/gitbucket/core/api/ApiPath.scala
Normal file
6
src/main/scala/gitbucket/core/api/ApiPath.scala
Normal file
@@ -0,0 +1,6 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
|
||||
*/
|
||||
case class ApiPath(path: String)
|
||||
25
src/main/scala/gitbucket/core/api/ApiPersonIdent.scala
Normal file
25
src/main/scala/gitbucket/core/api/ApiPersonIdent.scala
Normal file
@@ -0,0 +1,25 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
|
||||
import java.util.Date
|
||||
|
||||
|
||||
case class ApiPersonIdent(
|
||||
name: String,
|
||||
email: String,
|
||||
date: Date)
|
||||
|
||||
|
||||
object ApiPersonIdent {
|
||||
def author(commit: CommitInfo): ApiPersonIdent =
|
||||
ApiPersonIdent(
|
||||
name = commit.authorName,
|
||||
email = commit.authorEmailAddress,
|
||||
date = commit.authorTime)
|
||||
def committer(commit: CommitInfo): ApiPersonIdent =
|
||||
ApiPersonIdent(
|
||||
name = commit.committerName,
|
||||
email = commit.committerEmailAddress,
|
||||
date = commit.commitTime)
|
||||
}
|
||||
59
src/main/scala/gitbucket/core/api/ApiPullRequest.scala
Normal file
59
src/main/scala/gitbucket/core/api/ApiPullRequest.scala
Normal file
@@ -0,0 +1,59 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.{Issue, PullRequest}
|
||||
|
||||
import java.util.Date
|
||||
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/pulls/
|
||||
*/
|
||||
case class ApiPullRequest(
|
||||
number: Int,
|
||||
updated_at: Date,
|
||||
created_at: Date,
|
||||
head: ApiPullRequest.Commit,
|
||||
base: ApiPullRequest.Commit,
|
||||
mergeable: Option[Boolean],
|
||||
title: String,
|
||||
body: String,
|
||||
user: ApiUser) {
|
||||
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
|
||||
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
|
||||
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
|
||||
val url = ApiPath(s"${base.repo.url.path}/pulls/${number}")
|
||||
//val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}")
|
||||
val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits")
|
||||
val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments")
|
||||
val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}")
|
||||
val comments_url = ApiPath(s"${base.repo.url.path}/issues/${number}/comments")
|
||||
val statuses_url = ApiPath(s"${base.repo.url.path}/statuses/${head.sha}")
|
||||
}
|
||||
|
||||
object ApiPullRequest{
|
||||
def apply(issue: Issue, pullRequest: PullRequest, headRepo: ApiRepository, baseRepo: ApiRepository, user: ApiUser): ApiPullRequest = ApiPullRequest(
|
||||
number = issue.issueId,
|
||||
updated_at = issue.updatedDate,
|
||||
created_at = issue.registeredDate,
|
||||
head = Commit(
|
||||
sha = pullRequest.commitIdTo,
|
||||
ref = pullRequest.requestBranch,
|
||||
repo = headRepo)(issue.userName),
|
||||
base = Commit(
|
||||
sha = pullRequest.commitIdFrom,
|
||||
ref = pullRequest.branch,
|
||||
repo = baseRepo)(issue.userName),
|
||||
mergeable = None, // TODO: need check mergeable.
|
||||
title = issue.title,
|
||||
body = issue.content.getOrElse(""),
|
||||
user = user
|
||||
)
|
||||
|
||||
case class Commit(
|
||||
sha: String,
|
||||
ref: String,
|
||||
repo: ApiRepository)(baseOwner:String){
|
||||
val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
|
||||
val user = repo.owner
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
56
src/main/scala/gitbucket/core/api/ApiRepository.scala
Normal file
56
src/main/scala/gitbucket/core/api/ApiRepository.scala
Normal file
@@ -0,0 +1,56 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.{Account, Repository}
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
|
||||
|
||||
// https://developer.github.com/v3/repos/
|
||||
case class ApiRepository(
|
||||
name: String,
|
||||
full_name: String,
|
||||
description: String,
|
||||
watchers: Int,
|
||||
forks: Int,
|
||||
`private`: Boolean,
|
||||
default_branch: String,
|
||||
owner: ApiUser)(urlIsHtmlUrl: Boolean) {
|
||||
val forks_count = forks
|
||||
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}")
|
||||
}
|
||||
|
||||
object ApiRepository{
|
||||
def apply(
|
||||
repository: Repository,
|
||||
owner: ApiUser,
|
||||
forkedCount: Int =0,
|
||||
watchers: Int = 0,
|
||||
urlIsHtmlUrl: Boolean = false): ApiRepository =
|
||||
ApiRepository(
|
||||
name = repository.repositoryName,
|
||||
full_name = s"${repository.userName}/${repository.repositoryName}",
|
||||
description = repository.description.getOrElse(""),
|
||||
watchers = 0,
|
||||
forks = forkedCount,
|
||||
`private` = repository.isPrivate,
|
||||
default_branch = repository.defaultBranch,
|
||||
owner = owner
|
||||
)(urlIsHtmlUrl)
|
||||
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
|
||||
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
||||
this(repositoryInfo.repository, ApiUser(owner))
|
||||
|
||||
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
|
||||
|
||||
}
|
||||
36
src/main/scala/gitbucket/core/api/ApiUser.scala
Normal file
36
src/main/scala/gitbucket/core/api/ApiUser.scala
Normal file
@@ -0,0 +1,36 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Account
|
||||
|
||||
import java.util.Date
|
||||
|
||||
|
||||
case class ApiUser(
|
||||
login: String,
|
||||
email: String,
|
||||
`type`: String,
|
||||
site_admin: Boolean,
|
||||
created_at: Date) {
|
||||
val url = ApiPath(s"/api/v3/users/${login}")
|
||||
val html_url = ApiPath(s"/${login}")
|
||||
// val followers_url = ApiPath(s"/api/v3/users/${login}/followers")
|
||||
// val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}")
|
||||
// val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}")
|
||||
// val starred_url = ApiPath(s"/api/v3/users/${login}/starred{/owner}{/repo}")
|
||||
// val subscriptions_url = ApiPath(s"/api/v3/users/${login}/subscriptions")
|
||||
// val organizations_url = ApiPath(s"/api/v3/users/${login}/orgs")
|
||||
// val repos_url = ApiPath(s"/api/v3/users/${login}/repos")
|
||||
// val events_url = ApiPath(s"/api/v3/users/${login}/events{/privacy}")
|
||||
// val received_events_url = ApiPath(s"/api/v3/users/${login}/received_events")
|
||||
}
|
||||
|
||||
|
||||
object ApiUser{
|
||||
def apply(user: Account): ApiUser = ApiUser(
|
||||
login = user.userName,
|
||||
email = user.mailAddress,
|
||||
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" },
|
||||
site_admin = user.isAdmin,
|
||||
created_at = user.registeredDate
|
||||
)
|
||||
}
|
||||
7
src/main/scala/gitbucket/core/api/CreateAComment.scala
Normal file
7
src/main/scala/gitbucket/core/api/CreateAComment.scala
Normal file
@@ -0,0 +1,7 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||
* api form
|
||||
*/
|
||||
case class CreateAComment(body: String)
|
||||
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("-")
|
||||
}
|
||||
}
|
||||
26
src/main/scala/gitbucket/core/api/CreateAStatus.scala
Normal file
26
src/main/scala/gitbucket/core/api/CreateAStatus.scala
Normal file
@@ -0,0 +1,26 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.CommitState
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||
* api form
|
||||
*/
|
||||
case class CreateAStatus(
|
||||
/* state is Required. The state of the status. Can be one of pending, success, error, or failure. */
|
||||
state: String,
|
||||
/* context is a string label to differentiate this status from the status of other systems. Default: "default" */
|
||||
context: Option[String],
|
||||
/* The target URL to associate with this status. This URL will be linked from the GitHub UI to allow users to easily see the ‘source’ of the Status. */
|
||||
target_url: Option[String],
|
||||
/* description is a short description of the status.*/
|
||||
description: Option[String]
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
CommitState.valueOf(state).isDefined &&
|
||||
// only http
|
||||
target_url.filterNot(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length<255).isEmpty &&
|
||||
context.filterNot(f => f.length<255).isEmpty &&
|
||||
description.filterNot(f => f.length<1000).isEmpty
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/** export fields for json */
|
||||
trait FieldSerializable
|
||||
52
src/main/scala/gitbucket/core/api/JsonFormat.scala
Normal file
52
src/main/scala/gitbucket/core/api/JsonFormat.scala
Normal file
@@ -0,0 +1,52 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.format._
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.Serialization
|
||||
import java.util.Date
|
||||
import scala.util.Try
|
||||
|
||||
object JsonFormat {
|
||||
|
||||
case class Context(baseUrl: String)
|
||||
|
||||
val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
|
||||
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
|
||||
(
|
||||
{ case JString(s) => Try(parserISO.parseDateTime(s)).toOption.map(_.toDate).getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
|
||||
{ case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) }
|
||||
)
|
||||
) + FieldSerializer[ApiUser]() +
|
||||
FieldSerializer[ApiPullRequest]() +
|
||||
FieldSerializer[ApiRepository]() +
|
||||
FieldSerializer[ApiCommitListItem.Parent]() +
|
||||
FieldSerializer[ApiCommitListItem]() +
|
||||
FieldSerializer[ApiCommitListItem.Commit]() +
|
||||
FieldSerializer[ApiCommitStatus]() +
|
||||
FieldSerializer[FieldSerializable]() +
|
||||
FieldSerializer[ApiCombinedCommitStatus]() +
|
||||
FieldSerializer[ApiPullRequest.Commit]() +
|
||||
FieldSerializer[ApiIssue]() +
|
||||
FieldSerializer[ApiComment]()
|
||||
|
||||
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
|
||||
(
|
||||
{
|
||||
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
|
||||
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
|
||||
},
|
||||
{
|
||||
case ApiPath(path) => JString(c.baseUrl + path)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* convert object to json string
|
||||
*/
|
||||
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
|
||||
|
||||
}
|
||||
@@ -1,27 +1,35 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.account.html
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.helper
|
||||
import gitbucket.core.model.GroupMember
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.ssh.SshUtil
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util._
|
||||
|
||||
import service._
|
||||
import util._
|
||||
import util.StringUtil._
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import util.Implicits._
|
||||
import ssh.SshUtil
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import model.GroupMember
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
|
||||
class AccountController extends AccountControllerBase
|
||||
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||
with AccessTokenService with WebHookService
|
||||
|
||||
|
||||
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
|
||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||
with AccessTokenService with WebHookService =>
|
||||
|
||||
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||
url: Option[String], fileId: Option[String])
|
||||
@@ -31,6 +39,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
case class SshKeyForm(title: String, publicKey: String)
|
||||
|
||||
case class PersonalTokenForm(note: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||
@@ -54,6 +64,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
"publicKey" -> trim(label("Key" , text(required, validPublicKey)))
|
||||
)(SshKeyForm.apply)
|
||||
|
||||
val personalTokenForm = mapping(
|
||||
"note" -> trim(label("Token", text(required, maxlength(100))))
|
||||
)(PersonalTokenForm.apply)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
|
||||
|
||||
@@ -76,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()))
|
||||
@@ -103,21 +117,21 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
params.getOrElse("tab", "repositories") match {
|
||||
// Public Activity
|
||||
case "activity" =>
|
||||
_root_.account.html.activity(account,
|
||||
gitbucket.core.account.html.activity(account,
|
||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||
getActivitiesByUser(userName, true))
|
||||
|
||||
// Members
|
||||
case "members" if(account.isGroupAccount) => {
|
||||
val members = getGroupMembers(account.userName)
|
||||
_root_.account.html.members(account, members.map(_.userName),
|
||||
gitbucket.core.account.html.members(account, members.map(_.userName),
|
||||
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
||||
}
|
||||
|
||||
// Repositories
|
||||
case _ => {
|
||||
val members = getGroupMembers(account.userName)
|
||||
_root_.account.html.repositories(account,
|
||||
gitbucket.core.account.html.repositories(account,
|
||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
|
||||
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
||||
@@ -142,10 +156,29 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/users/#get-a-single-user
|
||||
*/
|
||||
get("/api/v3/users/:userName") {
|
||||
getAccountByUserName(params("userName")).map { account =>
|
||||
JsonFormat(ApiUser(account))
|
||||
} getOrElse NotFound
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||
*/
|
||||
get("/api/v3/user") {
|
||||
context.loginAccount.map { account =>
|
||||
JsonFormat(ApiUser(account))
|
||||
} getOrElse Unauthorized
|
||||
}
|
||||
|
||||
|
||||
get("/:userName/_edit")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { x =>
|
||||
account.html.edit(x, flash.get("info"))
|
||||
html.edit(x, flash.get("info"))
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
@@ -169,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))
|
||||
}
|
||||
|
||||
@@ -189,7 +223,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
get("/:userName/_ssh")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { x =>
|
||||
account.html.ssh(x, getPublicKeys(x.userName))
|
||||
html.ssh(x, getPublicKeys(x.userName))
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
@@ -206,12 +240,46 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
redirect(s"/${userName}/_ssh")
|
||||
})
|
||||
|
||||
get("/:userName/_application")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { x =>
|
||||
var tokens = getAccessTokens(x.userName)
|
||||
val generatedToken = flash.get("generatedToken") match {
|
||||
case Some((tokenId:Int, token:String)) => {
|
||||
val gt = tokens.find(_.accessTokenId == tokenId)
|
||||
gt.map{ t =>
|
||||
tokens = tokens.filterNot(_ == t)
|
||||
(t, token)
|
||||
}
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
html.application(x, tokens, generatedToken)
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { x =>
|
||||
val (tokenId, token) = generateAccessToken(userName, form.note)
|
||||
flash += "generatedToken" -> (tokenId, token)
|
||||
}
|
||||
redirect(s"/${userName}/_application")
|
||||
})
|
||||
|
||||
get("/:userName/_personalToken/delete/:id")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
val tokenId = params("id").toInt
|
||||
deleteAccessToken(userName, tokenId)
|
||||
redirect(s"/${userName}/_application")
|
||||
})
|
||||
|
||||
get("/register"){
|
||||
if(context.settings.allowAccountRegistration){
|
||||
if(context.loginAccount.isDefined){
|
||||
redirect("/")
|
||||
} else {
|
||||
account.html.register()
|
||||
html.register()
|
||||
}
|
||||
} else NotFound
|
||||
}
|
||||
@@ -225,7 +293,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
|
||||
get("/groups/new")(usersOnly {
|
||||
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
|
||||
html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
|
||||
})
|
||||
|
||||
post("/groups/new", newGroupForm)(usersOnly { form =>
|
||||
@@ -241,7 +309,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/:groupName/_editgroup")(managersOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -290,7 +358,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
* Show the new repository form.
|
||||
*/
|
||||
get("/new")(usersOnly {
|
||||
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
|
||||
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -299,56 +367,7 @@ 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)
|
||||
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||
}
|
||||
|
||||
// redirect to the repository
|
||||
@@ -356,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, context.baseUrl).isEmpty){
|
||||
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
|
||||
val repository = getRepository(owner, data.name, context.baseUrl).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, context.baseUrl).isEmpty){
|
||||
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
|
||||
val repository = getRepository(groupName, data.name, context.baseUrl).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
|
||||
@@ -366,7 +433,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val members = getGroupMembers(group)
|
||||
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
|
||||
}
|
||||
_root_.helper.html.forkrepository(
|
||||
helper.html.forkrepository(
|
||||
repository,
|
||||
(groups zip managerPermissions).toMap
|
||||
)
|
||||
@@ -400,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)
|
||||
|
||||
@@ -421,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")
|
||||
@@ -1,4 +1,4 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
class AnonymousAccessController extends AnonymousAccessControllerBase
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api.ApiError
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util._
|
||||
|
||||
import _root_.util.Directory._
|
||||
import _root_.util.Implicits._
|
||||
import _root_.util.ControlUtil._
|
||||
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
|
||||
import org.scalatra._
|
||||
import org.scalatra.json._
|
||||
import org.json4s._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import model._
|
||||
import service.{SystemSettingsService, AccountService}
|
||||
import org.json4s._
|
||||
import org.scalatra._
|
||||
import org.scalatra.i18n._
|
||||
import org.scalatra.json._
|
||||
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||
import org.scalatra.i18n._
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
|
||||
/**
|
||||
* Provides generic features for controller implementations.
|
||||
@@ -51,6 +57,9 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
// Git repository
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
if(path.startsWith("/api/v3/")){
|
||||
httpRequest.setAttribute(Keys.Request.APIv3, true)
|
||||
}
|
||||
// Scalatra actions
|
||||
super.doFilter(request, response, chain)
|
||||
}
|
||||
@@ -74,7 +83,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
}
|
||||
}
|
||||
|
||||
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
|
||||
private def LoginAccount: Option[Account] = request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
|
||||
|
||||
def ajaxGet(path : String)(action : => Any) : Route =
|
||||
super.get(path){
|
||||
@@ -103,13 +112,19 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
protected def NotFound() =
|
||||
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||
org.scalatra.NotFound()
|
||||
} else if(request.hasAttribute(Keys.Request.APIv3)){
|
||||
contentType = formats("json")
|
||||
org.scalatra.NotFound(ApiError("Not Found"))
|
||||
} else {
|
||||
org.scalatra.NotFound(html.error("Not Found"))
|
||||
org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
|
||||
}
|
||||
|
||||
protected def Unauthorized()(implicit context: app.Context) =
|
||||
protected def Unauthorized()(implicit context: Context) =
|
||||
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||
org.scalatra.Unauthorized()
|
||||
} else if(request.hasAttribute(Keys.Request.APIv3)){
|
||||
contentType = formats("json")
|
||||
org.scalatra.Unauthorized(ApiError("Requires authentication"))
|
||||
} else {
|
||||
if(context.loginAccount.isDefined){
|
||||
org.scalatra.Unauthorized(redirect("/"))
|
||||
@@ -146,6 +161,15 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
response.addHeader("X-Content-Type-Options", "nosniff")
|
||||
rawData
|
||||
}
|
||||
|
||||
// jenkins send message as 'application/x-www-form-urlencoded' but scalatra already parsed as multi-part-request.
|
||||
def extractFromJsonBody[A](implicit request:HttpServletRequest, mf:Manifest[A]): Option[A] = {
|
||||
(request.contentType.map(_.split(";").head.toLowerCase) match{
|
||||
case Some("application/x-www-form-urlencoded") => multiParams.keys.headOption.map(parse(_))
|
||||
case Some("application/json") => Some(parsedBody)
|
||||
case _ => Some(parse(request.body))
|
||||
}).filterNot(_ == JNothing).flatMap(j => Try(j.extract[A]).toOption)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +181,13 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
|
||||
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.
|
||||
@@ -1,9 +1,10 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service._
|
||||
import util.{StringUtil, UsersAuthenticator, Keys}
|
||||
import util.Implicits._
|
||||
import service.IssuesService.IssueSearchCondition
|
||||
import gitbucket.core.dashboard.html
|
||||
import gitbucket.core.service.{RepositoryService, PullRequestService, AccountService, IssuesService}
|
||||
import gitbucket.core.util.{StringUtil, Keys, UsersAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.service.IssuesService._
|
||||
|
||||
class DashboardController extends DashboardControllerBase
|
||||
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||
@@ -96,7 +97,7 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
dashboard.html.issues(
|
||||
html.issues(
|
||||
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
|
||||
@@ -119,7 +120,7 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
val allRepos = getAllRepositories(userName)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
dashboard.html.pulls(
|
||||
html.pulls(
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
|
||||
@@ -1,8 +1,8 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import util.{Keys, FileUtil}
|
||||
import util.ControlUtil._
|
||||
import util.Directory._
|
||||
import gitbucket.core.util.{Keys, FileUtil}
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Directory._
|
||||
import org.scalatra._
|
||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
||||
import org.apache.commons.io.FileUtils
|
||||
@@ -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)
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.helper.xml
|
||||
import gitbucket.core.html
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
|
||||
|
||||
import util._
|
||||
import util.Implicits._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
|
||||
class IndexController extends IndexControllerBase
|
||||
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
|
||||
|
||||
|
||||
trait IndexControllerBase extends ControllerBase {
|
||||
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
|
||||
|
||||
@@ -61,13 +68,13 @@ trait IndexControllerBase extends ControllerBase {
|
||||
|
||||
get("/activities.atom"){
|
||||
contentType = "application/atom+xml; type=feed"
|
||||
helper.xml.feed(getRecentActivities())
|
||||
xml.feed(getRecentActivities())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
private def signin(account: model.Account) = {
|
||||
private def signin(account: Account) = {
|
||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
@@ -92,15 +99,24 @@ trait IndexControllerBase extends ControllerBase {
|
||||
get("/_user/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* JSON APU for checking user existence.
|
||||
* JSON API for checking user existence.
|
||||
*/
|
||||
post("/_user/existence")(usersOnly {
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
})
|
||||
|
||||
/**
|
||||
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
|
||||
* but not enabled.
|
||||
*/
|
||||
get("/api/v3/rate_limit"){
|
||||
contentType = formats("json")
|
||||
// this message is same as github enterprise...
|
||||
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.issues.html
|
||||
import gitbucket.core.model.Issue
|
||||
import gitbucket.core.service.IssuesService._
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.Markdown
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import IssuesService._
|
||||
import util._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import org.scalatra.Ok
|
||||
import model.Issue
|
||||
|
||||
|
||||
class IssuesController extends IssuesControllerBase
|
||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
|
||||
|
||||
trait IssuesControllerBase extends ControllerBase {
|
||||
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
|
||||
|
||||
case class IssueCreateForm(title: String, content: Option[String],
|
||||
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
|
||||
@@ -60,7 +65,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
||||
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
|
||||
getIssue(owner, name, issueId) map {
|
||||
issues.html.issue(
|
||||
html.issue(
|
||||
_,
|
||||
getComments(owner, name, issueId.toInt),
|
||||
getIssueLabels(owner, name, issueId.toInt),
|
||||
@@ -73,9 +78,21 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
|
||||
(for{
|
||||
issueId <- params("id").toIntOpt
|
||||
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||
} yield {
|
||||
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
|
||||
}).getOrElse(NotFound)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
issues.html.create(
|
||||
html.create(
|
||||
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||
getMilestones(owner, name),
|
||||
getLabels(owner, name),
|
||||
@@ -109,15 +126,18 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
// record activity
|
||||
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
||||
|
||||
// extract references and create refer comment
|
||||
getIssue(owner, name, issueId.toString).foreach { issue =>
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||
}
|
||||
|
||||
// call web hooks
|
||||
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
@@ -160,6 +180,20 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
|
||||
(for{
|
||||
issueId <- params("id").toIntOpt
|
||||
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
|
||||
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
||||
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||
} yield {
|
||||
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
||||
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/${
|
||||
@@ -192,15 +226,25 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getIssue(repository.owner, repository.name, params("id")) map { x =>
|
||||
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
|
||||
params.get("dataType") collect {
|
||||
case t if t == "html" => issues.html.editissue(
|
||||
case t if t == "html" => html.editissue(
|
||||
x.content, x.issueId, x.userName, x.repositoryName)
|
||||
} getOrElse {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("title" -> x.title,
|
||||
"content" -> view.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
|
||||
@@ -210,30 +254,46 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getComment(repository.owner, repository.name, params("id")) map { x =>
|
||||
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
||||
params.get("dataType") collect {
|
||||
case t if t == "html" => issues.html.editcomment(
|
||||
case t if t == "html" => html.editcomment(
|
||||
x.content, x.commentId, x.userName, x.repositoryName)
|
||||
} getOrElse {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("content" -> view.Markdown.toHtml(x.content,
|
||||
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||
))
|
||||
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)
|
||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||
}
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
||||
defining(params("id").toInt){ issueId =>
|
||||
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -247,7 +307,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
milestoneId("milestoneId").map { milestoneId =>
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
|
||||
issues.milestones.html.progress(openCount + closeCount, closeCount)
|
||||
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
|
||||
} getOrElse NotFound
|
||||
} getOrElse Ok()
|
||||
})
|
||||
@@ -292,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
|
||||
@@ -301,7 +362,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
||||
|
||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
|
||||
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
|
||||
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
|
||||
@@ -312,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,13 +390,13 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||
*/
|
||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
||||
(getAction: model.Issue => Option[String] =
|
||||
(getAction: Issue => Option[String] =
|
||||
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
||||
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
val userName = context.loginAccount.get.userName
|
||||
|
||||
getIssue(owner, name, issueId.toString) map { issue =>
|
||||
getIssue(owner, name, issueId.toString) flatMap { issue =>
|
||||
val (action, recordActivity) =
|
||||
getAction(issue)
|
||||
.collect {
|
||||
@@ -346,11 +411,10 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
.getOrElse(None -> None)
|
||||
|
||||
val commentId = content
|
||||
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
|
||||
.getOrElse ( action.get.capitalize -> action.get )
|
||||
match {
|
||||
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
||||
val commentId = (content, action) match {
|
||||
case (None, None) => None
|
||||
case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
|
||||
case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
||||
}
|
||||
|
||||
// record comment activity if comment is entered
|
||||
@@ -365,23 +429,39 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
createReferComment(owner, name, issue, content)
|
||||
}
|
||||
|
||||
// call web hooks
|
||||
action match {
|
||||
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
|
||||
case Some(act) => val webHookAction = act match {
|
||||
case "open" => "opened"
|
||||
case "reopen" => "reopened"
|
||||
case "close" => "closed"
|
||||
case _ => act
|
||||
}
|
||||
if(issue.isPullRequest){
|
||||
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
|
||||
} else {
|
||||
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
|
||||
}
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier() match {
|
||||
case f =>
|
||||
content foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
|
||||
}
|
||||
}
|
||||
action foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
issue -> commentId
|
||||
commentId.map( issue -> _ )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,7 +475,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
val condition = session.putAndGet(sessionKey,
|
||||
if(request.hasQueryString){
|
||||
val q = request.getParameter("q")
|
||||
if(q == null){
|
||||
if(q == null || q.trim.isEmpty){
|
||||
IssueSearchCondition(request)
|
||||
} else {
|
||||
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
|
||||
@@ -403,11 +483,15 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
issues.html.list(
|
||||
html.list(
|
||||
"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),
|
||||
@@ -417,5 +501,4 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.issues.labels.html
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
||||
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import service._
|
||||
import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||
import util.Implicits._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.scalatra.Ok
|
||||
|
||||
@@ -23,7 +24,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
)(LabelForm.apply)
|
||||
|
||||
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||
issues.labels.html.list(
|
||||
html.list(
|
||||
getLabels(repository.owner, repository.name),
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
repository,
|
||||
@@ -31,12 +32,12 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
||||
issues.labels.html.edit(None, repository)
|
||||
html.edit(None, repository)
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
|
||||
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
||||
issues.labels.html.label(
|
||||
html.label(
|
||||
getLabel(repository.owner, repository.name, labelId).get,
|
||||
// TODO futility
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
@@ -46,13 +47,13 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
||||
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
||||
issues.labels.html.edit(Some(label), repository)
|
||||
html.edit(Some(label), repository)
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
|
||||
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
|
||||
issues.labels.html.label(
|
||||
html.label(
|
||||
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
|
||||
// TODO futility
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
@@ -1,11 +1,11 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.issues.milestones.html
|
||||
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
|
||||
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||
import util.Implicits._
|
||||
|
||||
class MilestonesController extends MilestonesControllerBase
|
||||
with MilestonesService with RepositoryService with AccountService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
@@ -23,7 +23,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
)(MilestoneForm.apply)
|
||||
|
||||
get("/:owner/:repository/issues/milestones")(referrersOnly { repository =>
|
||||
issues.milestones.html.list(
|
||||
html.list(
|
||||
params.getOrElse("state", "open"),
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name),
|
||||
repository,
|
||||
@@ -31,7 +31,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly {
|
||||
issues.milestones.html.edit(None, _)
|
||||
html.edit(None, _)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
||||
@@ -41,7 +41,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
|
||||
params("milestoneId").toIntOpt.map{ milestoneId =>
|
||||
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
|
||||
html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.admin.plugins.html
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.util.AdminAuthenticator
|
||||
|
||||
class PluginsController extends ControllerBase with AdminAuthenticator {
|
||||
get("/admin/plugins")(adminOnly {
|
||||
html.plugins(PluginRegistry().getPlugins())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
|
||||
import gitbucket.core.pulls.html
|
||||
import gitbucket.core.service.CommitStatusService
|
||||
import gitbucket.core.service.MergeService
|
||||
import gitbucket.core.service.IssuesService._
|
||||
import gitbucket.core.service.PullRequestService._
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.JGitUtil._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with CommitStatusService with MergeService
|
||||
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with CommitStatusService with MergeService =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||
|
||||
val pullRequestForm = mapping(
|
||||
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||
"content" -> trim(label("Content", optional(text()))),
|
||||
"targetUserName" -> trim(text(required, maxlength(100))),
|
||||
"targetBranch" -> trim(text(required, maxlength(100))),
|
||||
"requestUserName" -> trim(text(required, maxlength(100))),
|
||||
"requestRepositoryName" -> trim(text(required, maxlength(100))),
|
||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||
"commitIdTo" -> trim(text(required, maxlength(40))),
|
||||
"assignedUserName" -> trim(optional(text())),
|
||||
"milestoneId" -> trim(optional(number())),
|
||||
"labelNames" -> trim(optional(text()))
|
||||
)(PullRequestForm.apply)
|
||||
|
||||
val mergeForm = mapping(
|
||||
"message" -> trim(label("Message", text(required)))
|
||||
)(MergeForm.apply)
|
||||
|
||||
case class PullRequestForm(
|
||||
title: String,
|
||||
content: Option[String],
|
||||
targetUserName: String,
|
||||
targetBranch: String,
|
||||
requestUserName: String,
|
||||
requestRepositoryName: String,
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String,
|
||||
assignedUserName: Option[String],
|
||||
milestoneId: Option[Int],
|
||||
labelNames: Option[String]
|
||||
)
|
||||
|
||||
case class MergeForm(message: String)
|
||||
|
||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||
val q = request.getParameter("q")
|
||||
if(Option(q).exists(_.contains("is:issue"))){
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
|
||||
} else {
|
||||
searchPullRequests(None, repository)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/pulls/#list-pull-requests
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||
val page = IssueSearchCondition.page(request)
|
||||
// TODO: more api spec condition
|
||||
val condition = IssueSearchCondition(request)
|
||||
val baseOwner = getAccountByUserName(repository.owner).get
|
||||
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
|
||||
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
|
||||
ApiPullRequest(
|
||||
issue,
|
||||
pullRequest,
|
||||
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||
ApiRepository(repository, ApiUser(baseOwner)),
|
||||
ApiUser(issueUser)) })
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||
val (commits, diffs) =
|
||||
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
||||
html.pullreq(
|
||||
issue, pullreq,
|
||||
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
|
||||
.sortWith((a, b) => a.registeredDate before b.registeredDate),
|
||||
getIssueLabels(owner, name, issueId),
|
||||
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||
getMilestonesWithIssueCount(owner, name),
|
||||
getLabels(owner, name),
|
||||
commits,
|
||||
diffs,
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
repository)
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
|
||||
(for{
|
||||
issueId <- params("id").toIntOpt
|
||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||
} yield {
|
||||
JsonFormat(ApiPullRequest(
|
||||
issue,
|
||||
pullRequest,
|
||||
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||
ApiRepository(repository, ApiUser(baseOwner)),
|
||||
ApiUser(issueUser)))
|
||||
}).getOrElse(NotFound)
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
|
||||
val newId = git.getRepository.resolve(pullreq.commitIdTo)
|
||||
val repoFullName = RepositoryName(repository)
|
||||
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
|
||||
JsonFormat(commits)
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo)
|
||||
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
|
||||
checkConflict(owner, name, pullreq.branch, issueId)
|
||||
}
|
||||
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
|
||||
html.mergeguide(
|
||||
hasConfrict,
|
||||
hasProblem,
|
||||
issue,
|
||||
pullreq,
|
||||
statuses,
|
||||
repository,
|
||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
|
||||
params("id").toIntOpt.map { issueId =>
|
||||
val branchName = multiParams("splat").head
|
||||
val userName = context.loginAccount.get.userName
|
||||
if(repository.repository.defaultBranch != branchName){
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(branchName).call()
|
||||
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||
}
|
||||
}
|
||||
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||
params("id").toIntOpt.flatMap { issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
LockUtil.lock(s"${owner}/${name}"){
|
||||
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||
using(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// merge git repository
|
||||
mergePullRequest(git, pullreq.branch, issueId,
|
||||
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
|
||||
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
|
||||
if(pullreq.branch == defaultBranch){
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
}
|
||||
issue.content match {
|
||||
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
|
||||
case _ =>
|
||||
}
|
||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||
}
|
||||
|
||||
updatePullRequests(owner, name, pullreq.branch)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issue, "merge"){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
val headBranch:Option[String] = params.get("head")
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
||||
using(
|
||||
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ (oldGit, newGit) =>
|
||||
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
|
||||
val oldBranch = originRepository.branchList.find( _ == newBranch).getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
|
||||
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
case _ => {
|
||||
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
|
||||
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}")
|
||||
} getOrElse {
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner) {
|
||||
// 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 {
|
||||
// Sibling repository
|
||||
getUserRepositories(originOwner, context.baseUrl).find { x =>
|
||||
x.repository.originUserName == forkedRepository.repository.originUserName &&
|
||||
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
|
||||
}.map(_.repository.repositoryName)
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val (oldId, newId) =
|
||||
if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||
// Branch name
|
||||
val rootId = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||
originRepository.owner, originRepository.name, originId,
|
||||
forkedRepository.owner, forkedRepository.name, forkedId)
|
||||
|
||||
(Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
} else {
|
||||
// Commit id
|
||||
(Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
}
|
||||
|
||||
(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(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
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
|
||||
|
||||
(for(
|
||||
originRepositoryName <- if(originOwner == forkedOwner){
|
||||
Some(forkedRepository.name)
|
||||
} else {
|
||||
forkedRepository.repository.originRepositoryName.orElse {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}"){
|
||||
checkConflict(originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||
}
|
||||
html.mergecheck(conflict)
|
||||
}
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||
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 = 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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetch requested branch
|
||||
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
|
||||
|
||||
// record activity
|
||||
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
*
|
||||
* - "owner:branch" to ("owner", "branch")
|
||||
* - "branch" to ("defaultOwner", "branch")
|
||||
*/
|
||||
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
|
||||
if(value.contains(':')){
|
||||
val array = value.split(":")
|
||||
(array(0), array(1))
|
||||
} else {
|
||||
(defaultOwner, value)
|
||||
}
|
||||
|
||||
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
|
||||
using(
|
||||
Git.open(getRepositoryDir(userName, repositoryName)),
|
||||
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
|
||||
){ (oldGit, newGit) =>
|
||||
val oldId = oldGit.getRepository.resolve(branch)
|
||||
val newId = newGit.getRepository.resolve(requestCommitId)
|
||||
|
||||
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
|
||||
new CommitInfo(revCommit)
|
||||
}.toList.splitWith { (commit1, commit2) =>
|
||||
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
}
|
||||
|
||||
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
|
||||
|
||||
(commits, diffs)
|
||||
}
|
||||
|
||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
|
||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val sessionKey = Keys.Session.Pulls(owner, repoName)
|
||||
|
||||
// retrieve search condition
|
||||
val condition = session.putAndGet(sessionKey,
|
||||
if(request.hasQueryString) IssueSearchCondition(request)
|
||||
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
gitbucket.core.issues.html.list(
|
||||
"pulls",
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
page,
|
||||
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),
|
||||
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
|
||||
condition,
|
||||
repository,
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service._
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
|
||||
import gitbucket.core.settings.html
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.JGitUtil._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
import service.WebHookService.WebHookPayload
|
||||
import util.JGitUtil.CommitInfo
|
||||
import util.ControlUtil._
|
||||
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
|
||||
@@ -25,7 +29,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||
|
||||
val optionsForm = mapping(
|
||||
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||
"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()))
|
||||
@@ -39,10 +43,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
|
||||
@@ -63,7 +68,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the Options page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/options")(ownerOnly {
|
||||
settings.html.options(_, flash.get("info"))
|
||||
html.options(_, flash.get("info"))
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -105,7 +110,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the Collaborators page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
|
||||
settings.html.collaborators(
|
||||
html.collaborators(
|
||||
getCollaborators(repository.owner, repository.name),
|
||||
getAccountByUserName(repository.owner).get.isGroupAccount,
|
||||
repository)
|
||||
@@ -135,14 +140,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the web hook page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
|
||||
settings.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")
|
||||
})
|
||||
|
||||
@@ -150,30 +164,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 = 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(repository.owner, repository.name,
|
||||
List(model.WebHook(repository.owner, repository.name, form.url)),
|
||||
WebHookPayload(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")
|
||||
})
|
||||
|
||||
@@ -181,7 +252,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the danger zone.
|
||||
*/
|
||||
get("/:owner/:repository/settings/danger")(ownerOnly {
|
||||
settings.html.danger(_)
|
||||
html.danger(_)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -223,9 +294,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,35 +1,48 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import _root_.util.JGitUtil.CommitInfo
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import _root_.util.ControlUtil._
|
||||
import _root_.util._
|
||||
import service._
|
||||
import org.scalatra._
|
||||
import java.io.File
|
||||
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._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.JGitUtil._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.model.{Account, CommitState, WebHook}
|
||||
import gitbucket.core.service.CommitStatusService
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.api.{ArchiveCommand, Git}
|
||||
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.eclipse.jgit.errors.MissingObjectException
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import service.WebHookService.WebHookPayload
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import org.scalatra._
|
||||
|
||||
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService
|
||||
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
*/
|
||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService =>
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
|
||||
|
||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||
@@ -91,11 +104,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
*/
|
||||
post("/:owner/:repository/_preview")(referrersOnly { repository =>
|
||||
contentType = "text/html"
|
||||
view.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,
|
||||
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -105,6 +122,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
fileList(_)
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/#get
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
|
||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the file list of the specified path and branch.
|
||||
*/
|
||||
@@ -127,7 +151,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
||||
case Right((logs, hasNext)) =>
|
||||
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
||||
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
||||
logs.splitWith{ (commit1, commit2) =>
|
||||
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
@@ -136,9 +160,68 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
|
||||
(for{
|
||||
ref <- params.get("sha")
|
||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||
data <- extractFromJsonBody[CreateAStatus] if data.isValid
|
||||
creator <- context.loginAccount
|
||||
state <- CommitState.valueOf(data.state)
|
||||
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
|
||||
state, data.target_url, data.description, new java.util.Date(), creator)
|
||||
status <- getCommitStatus(repository.owner, repository.name, statusId)
|
||||
} yield {
|
||||
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||
*
|
||||
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||
*/
|
||||
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)
|
||||
} yield {
|
||||
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
|
||||
ApiCommitStatus(status, ApiUser(creator))
|
||||
})
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/repos/statuses/#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
|
||||
*
|
||||
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
|
||||
(for{
|
||||
ref <- params.get("ref")
|
||||
owner <- getAccountByUserName(repository.owner)
|
||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||
} yield {
|
||||
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
|
||||
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
|
||||
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
|
||||
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
|
||||
})
|
||||
|
||||
@@ -150,7 +233,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
val paths = path.split("/")
|
||||
repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
|
||||
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
|
||||
JGitUtil.getContentInfo(git, path, objectId))
|
||||
} getOrElse NotFound
|
||||
}
|
||||
@@ -163,33 +246,47 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
val paths = path.split("/")
|
||||
repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
|
||||
html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
|
||||
JGitUtil.getContentInfo(git, path, objectId))
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
|
||||
commitFile(repository, form.branch, form.path, Some(form.newFileName), None,
|
||||
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
|
||||
form.message.getOrElse(s"Create ${form.newFileName}"))
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = form.branch,
|
||||
path = form.path,
|
||||
newFileName = Some(form.newFileName),
|
||||
oldFileName = None,
|
||||
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||
charset = form.charset,
|
||||
message = form.message.getOrElse(s"Create ${form.newFileName}")
|
||||
)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
||||
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
|
||||
}")
|
||||
})
|
||||
|
||||
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
|
||||
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName,
|
||||
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
|
||||
if(form.oldFileName.exists(_ == form.newFileName)){
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = form.branch,
|
||||
path = form.path,
|
||||
newFileName = Some(form.newFileName),
|
||||
oldFileName = form.oldFileName,
|
||||
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||
charset = form.charset,
|
||||
message = if(form.oldFileName.exists(_ == form.newFileName)){
|
||||
form.message.getOrElse(s"Update ${form.newFileName}")
|
||||
} else {
|
||||
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
|
||||
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
|
||||
}")
|
||||
})
|
||||
|
||||
@@ -200,40 +297,93 @@ 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 = "application/octet-stream"
|
||||
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
|
||||
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
|
||||
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||
}
|
||||
// Download (This route is left for backword compatibility)
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
contentType = "application/octet-stream"
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
loader.copyTo(response.outputStream)
|
||||
()
|
||||
} getOrElse NotFound
|
||||
} else {
|
||||
repo.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) =>
|
||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||
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),
|
||||
@@ -241,12 +391,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
@@ -260,7 +413,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
val oldLineNumber = params.get("oldLineNumber") map (_.toInt)
|
||||
val newLineNumber = params.get("newLineNumber") map (_.toInt)
|
||||
val issueId = params.get("issueId") map (_.toInt)
|
||||
repo.html.commentform(
|
||||
html.commentform(
|
||||
commitId = id,
|
||||
fileName, oldLineNumber, newLineNumber, issueId,
|
||||
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
@@ -271,26 +424,36 @@ 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 =>
|
||||
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
|
||||
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
||||
params.get("dataType") collect {
|
||||
case t if t == "html" => repo.html.editcomment(
|
||||
case t if t == "html" => html.editcomment(
|
||||
x.content, x.commentId, x.userName, x.repositoryName)
|
||||
} getOrElse {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("content" -> view.Markdown.toHtml(x.content,
|
||||
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||
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
|
||||
@@ -322,14 +485,17 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
* Displays branches.
|
||||
*/
|
||||
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
// retrieve latest update date of each branch
|
||||
val branchInfo = repository.branchList.map { branchName =>
|
||||
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
|
||||
(branchName, revCommit.getCommitterIdent.getWhen)
|
||||
}
|
||||
repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||
}
|
||||
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))
|
||||
.reverse
|
||||
|
||||
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -369,7 +535,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
* Displays tags.
|
||||
*/
|
||||
get("/:owner/:repository/tags")(referrersOnly {
|
||||
repo.html.tags(_)
|
||||
html.tags(_)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -386,7 +552,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/network/members")(referrersOnly { repository =>
|
||||
repo.html.forked(
|
||||
html.forked(
|
||||
getRepository(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name),
|
||||
@@ -397,7 +563,36 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
repository)
|
||||
})
|
||||
|
||||
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
|
||||
/**
|
||||
* 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
|
||||
} orElse repository.tags.collectFirst {
|
||||
@@ -408,7 +603,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.
|
||||
@@ -420,7 +617,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
*/
|
||||
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
|
||||
if(repository.commitCount == 0){
|
||||
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} else {
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
// get specified commit
|
||||
@@ -439,14 +636,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
}
|
||||
|
||||
repo.html.files(revision, repository,
|
||||
html.files(revision, repository,
|
||||
if(path == ".") Nil else path.split("/").toList, // current path
|
||||
context.loginAccount match {
|
||||
case None => List()
|
||||
case account: Option[model.Account] => getGroupsByUserName(account.get.userName)
|
||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||
}, // groups of current user
|
||||
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
||||
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
||||
flash.get("info"), flash.get("error"))
|
||||
}
|
||||
} getOrElse NotFound
|
||||
@@ -454,7 +652,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
private def commitFile(repository: service.RepositoryService.RepositoryInfo,
|
||||
private def commitFile(repository: RepositoryService.RepositoryInfo,
|
||||
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
|
||||
content: String, charset: String, message: String) = {
|
||||
|
||||
@@ -506,14 +704,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
|
||||
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||
getWebHookURLs(repository.owner, repository.name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(ownerAccount <- getAccountByUserName(repository.owner)){
|
||||
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||
WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
|
||||
getAccountByUserName(repository.owner).map{ ownerAccount =>
|
||||
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
|
||||
oldId = headTip, newId = commitId)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,6 +759,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||
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,9 +1,10 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import util._
|
||||
import gitbucket.core.search.html
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
|
||||
import ControlUtil._
|
||||
import Implicits._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SearchController extends SearchControllerBase
|
||||
@@ -34,12 +35,12 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||
}
|
||||
|
||||
target.toLowerCase match {
|
||||
case "issue" => search.html.issues(
|
||||
case "issue" => html.issues(
|
||||
searchIssues(repository.owner, repository.name, query),
|
||||
countFiles(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
|
||||
case _ => search.html.code(
|
||||
case _ => html.code(
|
||||
searchFiles(repository.owner, repository.name, query),
|
||||
countIssues(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
@@ -1,10 +1,11 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service.{AccountService, SystemSettingsService}
|
||||
import gitbucket.core.admin.html
|
||||
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
||||
import gitbucket.core.util.AdminAuthenticator
|
||||
import gitbucket.core.ssh.SshServer
|
||||
import SystemSettingsService._
|
||||
import util.AdminAuthenticator
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import ssh.SshServer
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with AdminAuthenticator
|
||||
@@ -20,9 +21,11 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||
"ssh" -> trim(label("SSH access", boolean())),
|
||||
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||
"smtp" -> optionalIfNotChecked("notification", mapping(
|
||||
"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()))),
|
||||
@@ -59,7 +62,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
case class PluginForm(pluginIds: List[String])
|
||||
|
||||
get("/admin/system")(adminOnly {
|
||||
admin.html.system(flash.get("info"))
|
||||
html.system(flash.get("info"))
|
||||
})
|
||||
|
||||
post("/admin/system", form)(adminOnly { form =>
|
||||
@@ -70,7 +73,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
|
||||
SshServer.start(request.getServletContext,
|
||||
SshServer.start(
|
||||
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
|
||||
form.baseUrl.get)
|
||||
} else if(!form.ssh && SshServer.isActive){
|
||||
@@ -1,11 +1,12 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service._
|
||||
import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import util.ControlUtil._
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import gitbucket.core.service.{RepositoryService, AccountService}
|
||||
import gitbucket.core.admin.users.html
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.apache.commons.io.FileUtils
|
||||
@@ -75,11 +76,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||
}.toMap
|
||||
|
||||
admin.users.html.list(users, members, includeRemoved)
|
||||
html.list(users, members, includeRemoved)
|
||||
})
|
||||
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
admin.users.html.user(None)
|
||||
html.user(None)
|
||||
})
|
||||
|
||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||
@@ -90,7 +91,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
admin.users.html.user(getAccountByUserName(userName, true))
|
||||
html.user(getAccountByUserName(userName, true))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
@@ -99,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)
|
||||
}
|
||||
@@ -124,7 +125,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
get("/admin/users/_newgroup")(adminOnly {
|
||||
admin.users.html.group(None, Nil)
|
||||
html.group(None, Nil)
|
||||
})
|
||||
|
||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||
@@ -140,7 +141,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -193,7 +194,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
params.get(paramName).flatMap { userName =>
|
||||
if(userName == context.loginAccount.get.userName)
|
||||
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
||||
Some("You can't disable your account yourself")
|
||||
else
|
||||
None
|
||||
@@ -1,14 +1,15 @@
|
||||
package app
|
||||
package gitbucket.core.controller
|
||||
|
||||
import service._
|
||||
import util._
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import util.Implicits._
|
||||
import gitbucket.core.wiki.html
|
||||
import gitbucket.core.service.{RepositoryService, WikiService, ActivityService, AccountService}
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.i18n.Messages
|
||||
import java.util.ResourceBundle
|
||||
|
||||
class WikiController extends WikiControllerBase
|
||||
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||
@@ -36,7 +37,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
||||
getWikiPage(repository.owner, repository.name, "Home").map { page =>
|
||||
wiki.html.page("Home", page, getWikiPageList(repository.owner, repository.name),
|
||||
html.page("Home", page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
|
||||
})
|
||||
@@ -45,7 +46,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
|
||||
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
||||
wiki.html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
|
||||
html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
|
||||
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
|
||||
})
|
||||
@@ -55,7 +56,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
|
||||
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
|
||||
case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository)
|
||||
case Left(_) => NotFound
|
||||
}
|
||||
}
|
||||
@@ -66,7 +67,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
|
||||
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -75,7 +76,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -105,13 +106,21 @@ trait WikiControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
||||
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
||||
defining(context.loginAccount.get){ loginAccount =>
|
||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||
form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
|
||||
saveWikiPage(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.currentPageName,
|
||||
form.pageName,
|
||||
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
|
||||
loginAccount,
|
||||
form.message.getOrElse(""),
|
||||
Some(form.id)
|
||||
).map { commitId =>
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
||||
}
|
||||
@@ -120,7 +129,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
|
||||
wiki.html.edit("", None, _)
|
||||
html.edit("", None, _)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||
@@ -147,14 +156,14 @@ trait WikiControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
||||
wiki.html.pages(getWikiPageList(repository.owner, repository.name), repository,
|
||||
html.pages(getWikiPageList(repository.owner, repository.name), repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
JGitUtil.getCommitLog(git, "master") match {
|
||||
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
|
||||
case Right((logs, hasNext)) => html.history(None, logs, repository)
|
||||
case Left(_) => NotFound
|
||||
}
|
||||
}
|
||||
21
src/main/scala/gitbucket/core/model/AccessToken.scala
Normal file
21
src/main/scala/gitbucket/core/model/AccessToken.scala
Normal file
@@ -0,0 +1,21 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
|
||||
trait AccessTokenComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
lazy val AccessTokens = TableQuery[AccessTokens]
|
||||
|
||||
class AccessTokens(tag: Tag) extends Table[AccessToken](tag, "ACCESS_TOKEN") {
|
||||
val accessTokenId = column[Int]("ACCESS_TOKEN_ID", O AutoInc)
|
||||
val userName = column[String]("USER_NAME")
|
||||
val tokenHash = column[String]("TOKEN_HASH")
|
||||
val note = column[String]("NOTE")
|
||||
def * = (accessTokenId, userName, tokenHash, note) <> (AccessToken.tupled, AccessToken.unapply)
|
||||
}
|
||||
}
|
||||
case class AccessToken(
|
||||
accessTokenId: Int = 0,
|
||||
userName: String,
|
||||
tokenHash: String,
|
||||
note: String
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package model
|
||||
package gitbucket.core.model
|
||||
|
||||
trait AccountComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
@@ -1,4 +1,4 @@
|
||||
package model
|
||||
package gitbucket.core.model
|
||||
|
||||
trait ActivityComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
@@ -1,4 +1,4 @@
|
||||
package model
|
||||
package gitbucket.core.model
|
||||
|
||||
protected[model] trait TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
@@ -49,6 +49,9 @@ protected[model] trait TemplateComponent { self: Profile =>
|
||||
|
||||
def byCommit(owner: String, repository: String, commitId: String) =
|
||||
byRepository(owner, repository) && (this.commitId === commitId)
|
||||
|
||||
def byCommit(owner: Column[String], repository: Column[String], commitId: Column[String]) =
|
||||
byRepository(userName, repositoryName) && (this.commitId === commitId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package model
|
||||
package gitbucket.core.model
|
||||
|
||||
trait CollaboratorComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
@@ -1,4 +1,4 @@
|
||||
package model
|
||||
package gitbucket.core.model
|
||||
|
||||
trait Comment {
|
||||
val commentedUserName: String
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user