mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 01:37:34 +02:00
Compare commits
371 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c88b051121 | ||
|
|
c7776b5b37 | ||
|
|
f89afc175f | ||
|
|
1f252efdfb | ||
|
|
420ca85393 | ||
|
|
d60695992b | ||
|
|
3c0681d55d | ||
|
|
3fc0fa5a02 | ||
|
|
d84d40afea | ||
|
|
ddbbd38517 | ||
|
|
d588531ab8 | ||
|
|
bdc06feb88 | ||
|
|
3fc792fcf8 | ||
|
|
f5520e7991 | ||
|
|
897c5ecac7 | ||
|
|
4479ef31e2 | ||
|
|
ec827ab371 | ||
|
|
e79d463cf7 | ||
|
|
1508e5db49 | ||
|
|
8de391825a | ||
|
|
da1172a882 | ||
|
|
288a434598 | ||
|
|
1d6ae1e589 | ||
|
|
dd29456384 | ||
|
|
95a658defa | ||
|
|
cd298eb5c1 | ||
|
|
f7de3bab74 | ||
|
|
13578dcee8 | ||
|
|
6d76e93ede | ||
|
|
6b57cca64d | ||
|
|
e0bd5a24f4 | ||
|
|
2b2bf88a37 | ||
|
|
a0fa53e8cb | ||
|
|
c841d4a77a | ||
|
|
bf4b2dc72c | ||
|
|
078ed868fb | ||
|
|
bfc1d1d6b0 | ||
|
|
42ecae944e | ||
|
|
b9aa6a234b | ||
|
|
5f2d62030f | ||
|
|
fd0169d012 | ||
|
|
7e26b4695d | ||
|
|
cdfdff5c32 | ||
|
|
df5600f03f | ||
|
|
231fd268df | ||
|
|
582df3239f | ||
|
|
3ea102e238 | ||
|
|
52ab3c625e | ||
|
|
dee13542cd | ||
|
|
e90ba9e65b | ||
|
|
ca86076a02 | ||
|
|
6c75a29cb0 | ||
|
|
e10777576f | ||
|
|
08eaf2104b | ||
|
|
14de86afa0 | ||
|
|
69c5f9e19a | ||
|
|
03e903eef9 | ||
|
|
f3a1815bc5 | ||
|
|
ef03f77dc9 | ||
|
|
1a509a9a13 | ||
|
|
1e566f4a20 | ||
|
|
709c8f32b5 | ||
|
|
f2787a547f | ||
|
|
629b714dab | ||
|
|
1b0269c567 | ||
|
|
6158dc9607 | ||
|
|
5462f0a7a1 | ||
|
|
6d453ea80b | ||
|
|
5952648fae | ||
|
|
6b49bd557f | ||
|
|
c071284a56 | ||
|
|
5930cf48d5 | ||
|
|
9dd070887a | ||
|
|
cf687a0f2c | ||
|
|
f5c0cfdcdd | ||
|
|
03e2974709 | ||
|
|
d2373a00ea | ||
|
|
e769460397 | ||
|
|
a0a284ad26 | ||
|
|
1ebf4276e7 | ||
|
|
908931b9ed | ||
|
|
50655d1ac2 | ||
|
|
92e19ee19f | ||
|
|
52f3a90d18 | ||
|
|
11371c9e4f | ||
|
|
1b71b81953 | ||
|
|
c9d9d22215 | ||
|
|
5300641822 | ||
|
|
b31b7e1e86 | ||
|
|
cfb2f5beb9 | ||
|
|
ee9f24b2a6 | ||
|
|
8c86e23a4c | ||
|
|
fe98d35d4e | ||
|
|
8e10693402 | ||
|
|
f31848721c | ||
|
|
6101e141d8 | ||
|
|
71d84e7475 | ||
|
|
735ad4c972 | ||
|
|
50cb59f569 | ||
|
|
b58c19b88b | ||
|
|
6fe9ebbd2d | ||
|
|
4ea23a96ae | ||
|
|
ebf5d00fd2 | ||
|
|
b015645ed0 | ||
|
|
ce3b10ef03 | ||
|
|
d7af5551eb | ||
|
|
1d03a82be4 | ||
|
|
aa5fdfa395 | ||
|
|
7e05bcc81d | ||
|
|
e52aa7ad3c | ||
|
|
42faf9bda2 | ||
|
|
984164ba60 | ||
|
|
d26faac0e6 | ||
|
|
b54a9ace9f | ||
|
|
ad0131de66 | ||
|
|
b9ac48ebef | ||
|
|
71751ae4bc | ||
|
|
1c6f4a1d1e | ||
|
|
fd84b3f1c4 | ||
|
|
9d4a052ecc | ||
|
|
f93c8965be | ||
|
|
beef86ce8c | ||
|
|
03b75d5379 | ||
|
|
66855e65bb | ||
|
|
b8da93912f | ||
|
|
d675115615 | ||
|
|
0296a0bde6 | ||
|
|
8409384232 | ||
|
|
cfaee56a08 | ||
|
|
7d65717784 | ||
|
|
7079d50fdf | ||
|
|
41a613e151 | ||
|
|
1f2b6a0acc | ||
|
|
25d402c9d1 | ||
|
|
045b7cf019 | ||
|
|
57109dd72e | ||
|
|
7a8958741d | ||
|
|
f317d74bb4 | ||
|
|
5f0eb91a81 | ||
|
|
66f3a1fe7d | ||
|
|
59d85531ce | ||
|
|
4bd4c3e833 | ||
|
|
47f082e2fc | ||
|
|
1af52d16c0 | ||
|
|
2f52ed3ee0 | ||
|
|
a09407da8e | ||
|
|
1b878b59b8 | ||
|
|
80452ab4cd | ||
|
|
4d9c8e8d3e | ||
|
|
e15bd77789 | ||
|
|
a5f12a50e6 | ||
|
|
07ef06ad95 | ||
|
|
b61836adf7 | ||
|
|
34e2663492 | ||
|
|
8b90f87589 | ||
|
|
8c1e45da6c | ||
|
|
88caff38f0 | ||
|
|
62a6d74393 | ||
|
|
cb94447290 | ||
|
|
e4cf509d0f | ||
|
|
205119cc01 | ||
|
|
f10f98abf2 | ||
|
|
3a7391fbb3 | ||
|
|
2155734e23 | ||
|
|
6806e66d64 | ||
|
|
db8305b5e9 | ||
|
|
e8330eedc3 | ||
|
|
c01c4a860c | ||
|
|
6e778f209d | ||
|
|
b760361184 | ||
|
|
7150befa54 | ||
|
|
5bf0b275cb | ||
|
|
c86bf1d68b | ||
|
|
e61bde1415 | ||
|
|
e4b3f0ddef | ||
|
|
ec73294900 | ||
|
|
30eb949ce1 | ||
|
|
f5d69a3df6 | ||
|
|
3cc39489bd | ||
|
|
ace5d7de9e | ||
|
|
1682eb3915 | ||
|
|
6fd1a990ae | ||
|
|
cfa36a21b5 | ||
|
|
95163d4864 | ||
|
|
5a9645829d | ||
|
|
be78d93c1f | ||
|
|
ac63558645 | ||
|
|
88fb2e49dc | ||
|
|
6e96ad0f17 | ||
|
|
e54754d04f | ||
|
|
e4b2ebe2a4 | ||
|
|
0028431dde | ||
|
|
91d94de1d2 | ||
|
|
0c131ec990 | ||
|
|
54280d5572 | ||
|
|
6d3640a8b0 | ||
|
|
8226073506 | ||
|
|
f4a5e18c69 | ||
|
|
133af93548 | ||
|
|
3546a5d392 | ||
|
|
fb921e951e | ||
|
|
22685d8e3b | ||
|
|
b2d050d136 | ||
|
|
e3ff1dcd96 | ||
|
|
897890e1b4 | ||
|
|
2c95ea00e8 | ||
|
|
00d6ed7dbb | ||
|
|
b23133c79c | ||
|
|
eef2f26707 | ||
|
|
7483ad1732 | ||
|
|
188237db24 | ||
|
|
134624967b | ||
|
|
a1b8d1cd84 | ||
|
|
e7b9293f3b | ||
|
|
93e4a8931d | ||
|
|
323e25951f | ||
|
|
49d0c0de87 | ||
|
|
e31a835c4e | ||
|
|
dedf5094c1 | ||
|
|
fed4619a92 | ||
|
|
9eb1a20b3f | ||
|
|
ac21b9cc20 | ||
|
|
4a486e3bf8 | ||
|
|
269374e6bb | ||
|
|
e4d97e4059 | ||
|
|
ba567d81cb | ||
|
|
4fb6005f44 | ||
|
|
69ec4175eb | ||
|
|
d46e90dcdb | ||
|
|
900e91e101 | ||
|
|
05d7e33d86 | ||
|
|
7f0aff8c03 | ||
|
|
512e59193d | ||
|
|
8f056e4a82 | ||
|
|
d06a986293 | ||
|
|
d866847c0d | ||
|
|
83472bc354 | ||
|
|
ac784f8905 | ||
|
|
6035281ca1 | ||
|
|
ce8168d97a | ||
|
|
27670525a3 | ||
|
|
4796d7f450 | ||
|
|
79ec96343f | ||
|
|
4572a455c8 | ||
|
|
cb591925ea | ||
|
|
0bc6102096 | ||
|
|
3282a8d76a | ||
|
|
d04befb8d0 | ||
|
|
2c52a4c40c | ||
|
|
f53f71ecf1 | ||
|
|
fc7481c60c | ||
|
|
e59ae9c6e9 | ||
|
|
aae40a7087 | ||
|
|
9f9148fc1f | ||
|
|
20e5832ce3 | ||
|
|
fc29b34573 | ||
|
|
5ea9150af8 | ||
|
|
159a5835e0 | ||
|
|
78d48c8be3 | ||
|
|
9bb6b216e9 | ||
|
|
dc59d1f3ca | ||
|
|
1ab3f53a31 | ||
|
|
fd7d387fb0 | ||
|
|
17a64506f8 | ||
|
|
b68977597b | ||
|
|
2fb9f83227 | ||
|
|
6fd312f784 | ||
|
|
12d59231c5 | ||
|
|
3a7e2c0249 | ||
|
|
62f2defd91 | ||
|
|
9048e07b6b | ||
|
|
0903721a62 | ||
|
|
bf3380755b | ||
|
|
5d327ccd53 | ||
|
|
eb82af9006 | ||
|
|
2cc2902930 | ||
|
|
f4cb0625bc | ||
|
|
edd40ebe9d | ||
|
|
4e8c130cbf | ||
|
|
0760b6a89c | ||
|
|
f34f60b255 | ||
|
|
3c2675fd0d | ||
|
|
f163e348e0 | ||
|
|
71a3d79c82 | ||
|
|
bd1ba67647 | ||
|
|
60cd1320d2 | ||
|
|
a31de89f9c | ||
|
|
a129f53e0c | ||
|
|
6aa86ac2e3 | ||
|
|
28cafbcad2 | ||
|
|
991f60ce44 | ||
|
|
5dbeabcc58 | ||
|
|
b6bcebc588 | ||
|
|
2f3aa57d23 | ||
|
|
a123774bab | ||
|
|
7f56b50267 | ||
|
|
386f0dc142 | ||
|
|
bf90811cef | ||
|
|
72e2c6dca7 | ||
|
|
81fe467b20 | ||
|
|
07cdc6002d | ||
|
|
ee6d17d165 | ||
|
|
6dd1299dff | ||
|
|
d59e358caa | ||
|
|
5e1eb39b87 | ||
|
|
063170463f | ||
|
|
f8a9851bb3 | ||
|
|
b81a30ef12 | ||
|
|
62fb968c9a | ||
|
|
88b8567d2b | ||
|
|
7e4a295ef0 | ||
|
|
289ed85365 | ||
|
|
07dd459f3c | ||
|
|
f104fab593 | ||
|
|
0170f9b44a | ||
|
|
585d96949b | ||
|
|
46b3807f21 | ||
|
|
5e8a73e29d | ||
|
|
796a276b65 | ||
|
|
072290e544 | ||
|
|
5d5b642fa9 | ||
|
|
70761f4ac1 | ||
|
|
9d61f73e22 | ||
|
|
7a3c61a8d0 | ||
|
|
96872d7d41 | ||
|
|
485d6131d5 | ||
|
|
4893e9a58a | ||
|
|
79480c1d73 | ||
|
|
02c015574f | ||
|
|
653872df8e | ||
|
|
248079f041 | ||
|
|
2da756692b | ||
|
|
240a749b87 | ||
|
|
e4324258d3 | ||
|
|
2c33abe5d1 | ||
|
|
09ef1e0319 | ||
|
|
c091d96999 | ||
|
|
1978061a06 | ||
|
|
617370e822 | ||
|
|
0ed6a96781 | ||
|
|
b3c3bf51ba | ||
|
|
0e187fe888 | ||
|
|
43efcf3a99 | ||
|
|
ebc858aed9 | ||
|
|
f94af86ff9 | ||
|
|
da1c58bac6 | ||
|
|
c1c136f6c0 | ||
|
|
8a18119b53 | ||
|
|
777142b992 | ||
|
|
daa54029ed | ||
|
|
136a654639 | ||
|
|
f13e2c0d71 | ||
|
|
97101248a2 | ||
|
|
5150b4b1b6 | ||
|
|
a6d2381a68 | ||
|
|
29161feb49 | ||
|
|
1a4a1c2ccb | ||
|
|
96dac65e31 | ||
|
|
6005282d9f | ||
|
|
129020dbc4 | ||
|
|
54e0242030 | ||
|
|
0e57f4064f | ||
|
|
e50c4528a6 | ||
|
|
342810aa3a | ||
|
|
f84078c7ca | ||
|
|
eba81a6065 | ||
|
|
427e9197d8 | ||
|
|
67d6cf37a5 | ||
|
|
e6451c7ede | ||
|
|
bcd88a1342 | ||
|
|
5ea250d89d |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,3 +14,7 @@ project/plugins/project/
|
||||
.classpath
|
||||
.project
|
||||
.cache
|
||||
|
||||
# IntelliJ specific
|
||||
.idea/
|
||||
.idea_modules/
|
||||
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
54
README.md
54
README.md
@@ -1,25 +1,26 @@
|
||||
GitBucket
|
||||
=========
|
||||
|
||||
GitBucket is a Github clone by Scala, Easy to setup.
|
||||
GitBucket is the easily installable Github clone written with Scala.
|
||||
|
||||
The current version of GitBucket provides a basic features below:
|
||||
|
||||
- Public / Private Git repository (http access only)
|
||||
- Repository viewer (some advanced features are not implemented)
|
||||
- Repository search (Code and Issues)
|
||||
- Wiki
|
||||
- Issues
|
||||
- Fork / Pull request
|
||||
- Mail notification
|
||||
- Activity timeline
|
||||
- User management (for Administrators)
|
||||
- Group (like Organization in Github)
|
||||
|
||||
Following features are not implemented, but we will make them in the future release!
|
||||
|
||||
- Fork and pull request
|
||||
- Timeline
|
||||
- Search
|
||||
- Network graph
|
||||
- Statics
|
||||
- Watch / Star
|
||||
- Team management (like Organization in Github)
|
||||
|
||||
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
|
||||
|
||||
@@ -32,9 +33,50 @@ Installation
|
||||
|
||||
The default administrator account is **root** and password is **root**.
|
||||
|
||||
To upgrade GitBucket, only replace gitbucket.war.
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 1.5 - 4 Sep 2013
|
||||
- Fork and pull request.
|
||||
- LDAP authentication.
|
||||
- Mail notification.
|
||||
- Add an option to turn off the gravatar support.
|
||||
- Add the branch tab in the repository viewer.
|
||||
- Encoding auto detection for the file content in the repository viewer.
|
||||
- Add favicon, header logo and icons for the timeline.
|
||||
- Specify data directory via environment variable GITBUCKET_HOME.
|
||||
- Fixed some bugs.
|
||||
|
||||
### 1.4 - 31 Jul 2013
|
||||
- Group management.
|
||||
- Repository search for code and issues.
|
||||
- Display user related issues on the dashboard.
|
||||
- Display participants avatar of issues on the issue page.
|
||||
- Performance improvement for repository viewer.
|
||||
- Alert by milestone due date.
|
||||
- H2 database administration console.
|
||||
- Fixed some bugs.
|
||||
|
||||
### 1.3 - 18 Jul 2013
|
||||
- Batch updating for issues.
|
||||
- Display assigned user on issue list.
|
||||
- User icon and Gravatar support.
|
||||
- Convert @xxxx to link to the account page.
|
||||
- Add copy to clipboard button for git clone URL.
|
||||
- Allows multi-byte characters as wiki page name.
|
||||
- Allows to create the empty repository.
|
||||
- Fixed some bugs.
|
||||
|
||||
### 1.2 - 09 Jul 2013
|
||||
- Added activity timeline.
|
||||
- Bugfix for Git 1.8.1.5 or later.
|
||||
- Allows multi-byte characters as label.
|
||||
- Fixed some bugs.
|
||||
|
||||
### 1.1 - 05 Jul 2013
|
||||
- Fixed some bugs.
|
||||
- Upgrade to JGit 3.0.
|
||||
|
||||
### 1.0 - 04 Jul 2013
|
||||
|
||||
- This is a first public release.
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>37</x>
|
||||
<y>36</y>
|
||||
<x>33</x>
|
||||
<y>18</y>
|
||||
</constraint>
|
||||
<sourceConnections/>
|
||||
<targetConnections>
|
||||
@@ -51,8 +51,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>751</x>
|
||||
<y>47</y>
|
||||
<x>723</x>
|
||||
<y>138</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
@@ -79,8 +79,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>882</x>
|
||||
<y>239</y>
|
||||
<x>1182</x>
|
||||
<y>339</y>
|
||||
</constraint>
|
||||
<sourceConnections/>
|
||||
<targetConnections>
|
||||
@@ -108,8 +108,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>940</x>
|
||||
<y>615</y>
|
||||
<x>1301</x>
|
||||
<y>836</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||
@@ -138,8 +138,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>420</x>
|
||||
<y>758</y>
|
||||
<x>684</x>
|
||||
<y>858</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||
@@ -167,8 +167,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>307</x>
|
||||
<y>356</y>
|
||||
<x>293</x>
|
||||
<y>478</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
@@ -210,8 +210,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>641</x>
|
||||
<y>569</y>
|
||||
<x>875</x>
|
||||
<y>677</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
@@ -283,9 +283,14 @@
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>MILESTONE_NAME</columnName>
|
||||
<logicalName>Milestone Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel/columnType"/>
|
||||
<columnName>TITLE</columnName>
|
||||
<logicalName>Title</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>VARCHAR</name>
|
||||
<logicalName>文字列</logicalName>
|
||||
<supportSize>true</supportSize>
|
||||
<type>12</type>
|
||||
</columnType>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
@@ -293,6 +298,49 @@
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>DESCRIPTION</columnName>
|
||||
<logicalName>Description</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>TEXT</name>
|
||||
<logicalName>文字列</logicalName>
|
||||
<supportSize>true</supportSize>
|
||||
<type>2005</type>
|
||||
</columnType>
|
||||
<size></size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>DUE_DATE</columnName>
|
||||
<logicalName>Due Date</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>TIMESTAMP</name>
|
||||
<logicalName>日時</logicalName>
|
||||
<supportSize>false</supportSize>
|
||||
<type>93</type>
|
||||
</columnType>
|
||||
<size>10</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>CLOSED_DATE</columnName>
|
||||
<logicalName>Closed Date</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
||||
<size>10</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</columns>
|
||||
<indices/>
|
||||
<backgroundColor>
|
||||
@@ -350,6 +398,36 @@
|
||||
</entry>
|
||||
</references>
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../../../../../../../.."/>
|
||||
<foreignKeyName>ISSUE_FK_2</foreignKeyName>
|
||||
<references>
|
||||
<entry>
|
||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ASSIGNED_USER_NAME</columnName>
|
||||
<logicalName>Assinged User Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</entry>
|
||||
</references>
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
</sourceConnections>
|
||||
<targetConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
@@ -375,8 +453,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>26</x>
|
||||
<y>660</y>
|
||||
<x>18</x>
|
||||
<y>776</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||
@@ -462,6 +540,22 @@
|
||||
<autoIncrement>true</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ACTION</columnName>
|
||||
<logicalName>Action</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>VARCHAR</name>
|
||||
<logicalName>文字列</logicalName>
|
||||
<supportSize>true</supportSize>
|
||||
<type>12</type>
|
||||
</columnType>
|
||||
<size>20</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description>Expand to VARCHAR(20) from VARCHAR(10) in 1.3</description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>CONTENT</columnName>
|
||||
@@ -498,7 +592,7 @@
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>UPDATED_DATE</columnName>
|
||||
<logicalName>Updated Date</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
|
||||
<size>10</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
@@ -572,10 +666,11 @@
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>TITLE</columnName>
|
||||
<logicalName>Title</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
||||
<size></size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
@@ -586,7 +681,7 @@
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>CONTENT</columnName>
|
||||
<logicalName>Content</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
||||
<size></size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
@@ -597,7 +692,7 @@
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>REGISTERED_DATE</columnName>
|
||||
<logicalName>Registered Date</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
|
||||
<size>10</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
@@ -608,7 +703,7 @@
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>UPDATED_DATE</columnName>
|
||||
<logicalName>Updated Date</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
|
||||
<size>10</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
@@ -801,8 +896,8 @@
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>388</x>
|
||||
<y>166</y>
|
||||
<x>481</x>
|
||||
<y>361</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||
@@ -862,6 +957,250 @@
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel">
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>1199</x>
|
||||
<y>25</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../.."/>
|
||||
<foreignKeyName>ACTIVITY_FK_2</foreignKeyName>
|
||||
<references>
|
||||
<entry>
|
||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ACTIVITY_USER_NAME</columnName>
|
||||
<logicalName>Activity User Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</entry>
|
||||
</references>
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
</sourceConnections>
|
||||
<targetConnections/>
|
||||
<error></error>
|
||||
<linkedPath></linkedPath>
|
||||
<tableName>ACTIVITY</tableName>
|
||||
<logicalName>Activity</logicalName>
|
||||
<description>Since 1.2</description>
|
||||
<columns>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ACTIVITY_ID</columnName>
|
||||
<logicalName>Activity ID</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>INT</name>
|
||||
<logicalName>整数</logicalName>
|
||||
<supportSize>false</supportSize>
|
||||
<type>4</type>
|
||||
</columnType>
|
||||
<size>10</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>true</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>true</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>USER_NAME</columnName>
|
||||
<logicalName>User Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>REPOSITORY_NAME</columnName>
|
||||
<logicalName>Repository Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ACTIVITY_TYPE</columnName>
|
||||
<logicalName>Activity Type</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>MESSAGE</columnName>
|
||||
<logicalName>Message</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||
<size></size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ADDITIONAL_INFO</columnName>
|
||||
<logicalName>Additional Information</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||
<size></size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ACTIVITY_DATE</columnName>
|
||||
<logicalName>Activity Date</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
|
||||
<size>10</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</columns>
|
||||
<indices/>
|
||||
<backgroundColor>
|
||||
<red>255</red>
|
||||
<green>255</green>
|
||||
<blue>206</blue>
|
||||
</backgroundColor>
|
||||
<sql></sql>
|
||||
</source>
|
||||
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||
<foreignKeyName>ACTIVITY_FK_1</foreignKeyName>
|
||||
<references/>
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel">
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>1451</x>
|
||||
<y>577</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||
</sourceConnections>
|
||||
<targetConnections/>
|
||||
<error></error>
|
||||
<linkedPath></linkedPath>
|
||||
<tableName>COMMIT_LOG</tableName>
|
||||
<logicalName>Commit Log</logicalName>
|
||||
<description>Since 1.2</description>
|
||||
<columns>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>USER_NAME</columnName>
|
||||
<logicalName>User Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>true</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>REPOSITORY_NAME</columnName>
|
||||
<logicalName>Repository Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>true</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>COMMIT_ID</columnName>
|
||||
<logicalName>Commit ID</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>40</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>true</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</columns>
|
||||
<indices/>
|
||||
<backgroundColor>
|
||||
<red>255</red>
|
||||
<green>255</green>
|
||||
<blue>206</blue>
|
||||
</backgroundColor>
|
||||
<sql></sql>
|
||||
</source>
|
||||
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||
<foreignKeyName>COMMIT_LOG_FK_1</foreignKeyName>
|
||||
<references/>
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
</targetConnections>
|
||||
<error></error>
|
||||
<linkedPath></linkedPath>
|
||||
@@ -1062,6 +1401,100 @@
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]"/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]"/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel">
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>432</x>
|
||||
<y>240</y>
|
||||
</constraint>
|
||||
<sourceConnections>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../.."/>
|
||||
<foreignKeyName>GROUP_MEMBER_FK_2</foreignKeyName>
|
||||
<references/>
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
</sourceConnections>
|
||||
<targetConnections/>
|
||||
<error></error>
|
||||
<linkedPath></linkedPath>
|
||||
<tableName>GROUP_MEMBER</tableName>
|
||||
<logicalName>Group Member</logicalName>
|
||||
<description>Since 1.4</description>
|
||||
<columns>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>GROUP_NAME</columnName>
|
||||
<logicalName>Group Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>true</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>USER_NAME</columnName>
|
||||
<logicalName>User Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>true</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</columns>
|
||||
<indices/>
|
||||
<backgroundColor>
|
||||
<red>255</red>
|
||||
<green>255</green>
|
||||
<blue>206</blue>
|
||||
</backgroundColor>
|
||||
<sql></sql>
|
||||
</source>
|
||||
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
|
||||
<foreignKeyName>GROUP_MEMBER_FK_1</foreignKeyName>
|
||||
<references>
|
||||
<entry>
|
||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||
<net.java.amateras.db.visual.model.ColumnModel reference="../../../source/columns/net.java.amateras.db.visual.model.ColumnModel"/>
|
||||
</entry>
|
||||
</references>
|
||||
</net.java.amateras.db.visual.model.ForeignKeyModel>
|
||||
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
|
||||
</targetConnections>
|
||||
<error></error>
|
||||
<linkedPath></linkedPath>
|
||||
@@ -1089,8 +1522,8 @@
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>PASSWORD</columnName>
|
||||
<logicalName>Password</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[2]/columnType"/>
|
||||
<size>20</size>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
|
||||
<size>40</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
@@ -1098,18 +1531,18 @@
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>USER_TYPE</columnName>
|
||||
<logicalName>User Type</logicalName>
|
||||
<columnName>ADMINISTRATOR</columnName>
|
||||
<logicalName>Administrator</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>INT</name>
|
||||
<logicalName>整数</logicalName>
|
||||
<name>BOOLEAN</name>
|
||||
<logicalName>真偽値</logicalName>
|
||||
<supportSize>false</supportSize>
|
||||
<type>4</type>
|
||||
<type>16</type>
|
||||
</columnType>
|
||||
<size>10</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description>0:Normal 1:Administrator</description>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue>0</defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
@@ -1157,6 +1590,33 @@
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>IMAGE</columnName>
|
||||
<logicalName>Image</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description>Since 1.3</description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>GROUP_ACCOUNT</columnName>
|
||||
<logicalName>Group Account</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>BOOLEAN</name>
|
||||
<logicalName>真偽値</logicalName>
|
||||
<supportSize>false</supportSize>
|
||||
<type>16</type>
|
||||
</columnType>
|
||||
<size>10</size>
|
||||
<notNull>true</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description>Since 1.4</description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue>FALSE</defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</columns>
|
||||
<indices>
|
||||
<net.java.amateras.db.visual.model.IndexModel>
|
||||
@@ -1184,6 +1644,91 @@
|
||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target"/>
|
||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source"/>
|
||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/source"/>
|
||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
|
||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[7]/source"/>
|
||||
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
|
||||
<net.java.amateras.db.visual.model.TableModel>
|
||||
<listeners serialization="custom">
|
||||
<java.beans.PropertyChangeSupport>
|
||||
<default>
|
||||
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
|
||||
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
|
||||
</default>
|
||||
<null/>
|
||||
</java.beans.PropertyChangeSupport>
|
||||
</listeners>
|
||||
<constraint>
|
||||
<height>-1</height>
|
||||
<width>-1</width>
|
||||
<x>410</x>
|
||||
<y>860</y>
|
||||
</constraint>
|
||||
<sourceConnections/>
|
||||
<targetConnections/>
|
||||
<error></error>
|
||||
<linkedPath></linkedPath>
|
||||
<tableName>ISSUE_OUTLINE_VIEW</tableName>
|
||||
<logicalName>Issue Outline View</logicalName>
|
||||
<description>Since 1.4</description>
|
||||
<columns>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>USER_NAME</columnName>
|
||||
<logicalName>User Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>REPOSITORY_NAME</columnName>
|
||||
<logicalName>Repository Name</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
|
||||
<size>100</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>ISSUE_ID</columnName>
|
||||
<logicalName>Issue ID</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType">
|
||||
<name>INT</name>
|
||||
<logicalName>整数</logicalName>
|
||||
<supportSize>false</supportSize>
|
||||
<type>4</type>
|
||||
</columnType>
|
||||
<size>10</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
<net.java.amateras.db.visual.model.ColumnModel>
|
||||
<columnName>COMMENT_COUNT</columnName>
|
||||
<logicalName>Comment Count</logicalName>
|
||||
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[3]/columnType"/>
|
||||
<size>10</size>
|
||||
<notNull>false</notNull>
|
||||
<primaryKey>false</primaryKey>
|
||||
<description></description>
|
||||
<autoIncrement>false</autoIncrement>
|
||||
<defaultValue></defaultValue>
|
||||
</net.java.amateras.db.visual.model.ColumnModel>
|
||||
</columns>
|
||||
<indices/>
|
||||
<backgroundColor>
|
||||
<red>210</red>
|
||||
<green>232</green>
|
||||
<blue>249</blue>
|
||||
</backgroundColor>
|
||||
<sql></sql>
|
||||
</net.java.amateras.db.visual.model.TableModel>
|
||||
</children>
|
||||
<dommains/>
|
||||
<dialectName>H2</dialectName>
|
||||
751
etc/icons.svg
Normal file
751
etc/icons.svg
Normal file
@@ -0,0 +1,751 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="744.09448819"
|
||||
height="1052.3622047"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="icons.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="629.30023"
|
||||
inkscape:cy="281.44758"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1-9"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true"
|
||||
inkscape:snap-grids="false"
|
||||
inkscape:snap-page="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="false"
|
||||
inkscape:bbox-nodes="false"
|
||||
inkscape:snap-to-guides="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3080" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="layer1-9"
|
||||
inkscape:label="Layer 1"
|
||||
transform="matrix(0.66004549,0,0,0.66004549,12.445368,29.409765)">
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.51504707px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 865.73247,686.51304 c 0,0 19.28074,14.1795 55.09542,13.7739 35.81468,-0.4056 45.91286,-13.7739 45.91286,-13.7739 l 31.84606,-118.8515 -163.46293,0 z"
|
||||
id="path4000"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="czcccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#b3b3b3;stroke-width:25.84518814;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="m 306.9072,1201.5096 c 0,0 3.44333,-28.5633 47.63498,-35.4849 15.10377,-2.3655 48.7968,-8.2798 48.7968,-42.5816"
|
||||
id="path3207"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:transform-center-x="-6.1348784"
|
||||
sodipodi:nodetypes="csc"
|
||||
inkscape:transform-center-y="1.9434039e-005" />
|
||||
<path
|
||||
style="fill:none;stroke:#b3b3b3;stroke-width:26.60422707;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="m 76.384718,1086.1545 c 0,82.8617 105.181182,77.9295 105.181182,77.9295"
|
||||
id="path4318"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.18291342;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect3935"
|
||||
width="266.2222"
|
||||
height="35.127476"
|
||||
x="-4.6761055"
|
||||
y="865.6405" />
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:11.34059906;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="m 664.11762,675.10023 -74.94096,87.54344 20.17642,-92.15099 z"
|
||||
id="path3894-1"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:28.84111404;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect3088-5-5"
|
||||
width="169.03172"
|
||||
height="105.81662"
|
||||
x="547.64557"
|
||||
y="573.36456" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3850"
|
||||
d="m 445.03908,191.42833 0,-128.577242 c 0,0 1.85983,-15.30681 -16.73849,-15.30681 -18.59831,0 -51.14538,0 -51.14538,0"
|
||||
style="fill:none;stroke:#008000;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path2991"
|
||||
transform="translate(-137.57539,-163.64471)"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#008000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
id="path2993"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.83611704,0,0,0.83611704,-94.824045,-115.22257)" />
|
||||
<rect
|
||||
id="rect2995"
|
||||
y="54.447956"
|
||||
x="104.3765"
|
||||
height="99.221695"
|
||||
width="29.189819"
|
||||
style="fill:#008000;stroke:#ffffff;stroke-width:1.11112404" />
|
||||
<rect
|
||||
id="rect2997"
|
||||
y="173.24185"
|
||||
x="104.63474"
|
||||
height="26.258072"
|
||||
width="29.724136"
|
||||
style="fill:#008000;stroke:#ffffff;stroke-width:0.57680577" />
|
||||
<rect
|
||||
y="68.361099"
|
||||
x="330.18893"
|
||||
height="104.27071"
|
||||
width="3.2554624"
|
||||
id="rect3818"
|
||||
style="fill:#ffffff;stroke:#008000;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-20.394061,56.890898)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4"
|
||||
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-21.929587,-93.432709)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795"
|
||||
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,92.394578,56.992418)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-0"
|
||||
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3852"
|
||||
d="m 404.75446,10.803052 0,70.691447 L 359.1655,49.35988 z"
|
||||
style="fill:#008000;stroke:#008000;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3850-4"
|
||||
d="m 448.69288,446.18012 0,-128.57725 c 0,0 1.85984,-15.30681 -16.73848,-15.30681 -18.59831,0 -51.14539,0 -51.14539,0"
|
||||
style="fill:none;stroke:#800000;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path2991-8"
|
||||
transform="translate(-133.92158,91.107081)"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#800000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
id="path2993-8"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.83611704,0,0,0.83611704,-91.170233,139.52922)" />
|
||||
<rect
|
||||
id="rect2995-2"
|
||||
y="309.19974"
|
||||
x="108.03028"
|
||||
height="99.221687"
|
||||
width="29.189819"
|
||||
style="fill:#800000;stroke:#ffffff;stroke-width:1.11112404" />
|
||||
<rect
|
||||
id="rect2997-4"
|
||||
y="427.99362"
|
||||
x="108.28852"
|
||||
height="26.258072"
|
||||
width="29.724136"
|
||||
style="fill:#800000;stroke:#ffffff;stroke-width:0.57680577" />
|
||||
<rect
|
||||
y="323.11288"
|
||||
x="333.84274"
|
||||
height="104.27072"
|
||||
width="3.2554622"
|
||||
id="rect3818-5"
|
||||
style="fill:#ffffff;stroke:#800000;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-16.740254,311.64269)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-5"
|
||||
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-18.275774,161.31908)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-1"
|
||||
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,96.048392,311.7442)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-0-7"
|
||||
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3852-1"
|
||||
d="m 408.40826,265.55484 0,70.69144 -45.58895,-32.13461 z"
|
||||
style="fill:#800000;stroke:#800000;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#cccccc"
|
||||
id="rect2985"
|
||||
width="308.26331"
|
||||
height="308.26331"
|
||||
x="647.59973"
|
||||
y="19.593252" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:#ffffff"
|
||||
id="path2989"
|
||||
sodipodi:cx="246.42857"
|
||||
sodipodi:cy="327.36218"
|
||||
sodipodi:rx="35"
|
||||
sodipodi:ry="35"
|
||||
d="m 281.42857,327.36218 c 0,19.32997 -15.67003,35 -35,35 -19.32996,0 -35,-15.67003 -35,-35 0,-19.32996 15.67004,-35 35,-35 19.32997,0 35,15.67004 35,35 z"
|
||||
transform="matrix(2.9255147,0,0,2.9255147,83.281176,-813.70029)" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:#ffffff;stroke-width:1.59620917px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 715.46559,327.54005 179.96463,0 -89.85466,-201.67002 z"
|
||||
id="path2993-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3850-1"
|
||||
d="m 447.16245,696.53224 0,-128.57724 c 0,0 1.85984,-15.30681 -16.73848,-15.30681 -18.59831,0 -51.14539,0 -51.14539,0"
|
||||
style="fill:none;stroke:#b3b3b3;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path2991-7"
|
||||
transform="translate(-135.45201,341.45921)"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
id="path2993-4"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.83611704,0,0,0.83611704,-92.700665,389.88135)" />
|
||||
<rect
|
||||
id="rect2995-0"
|
||||
y="559.55188"
|
||||
x="106.49989"
|
||||
height="99.221687"
|
||||
width="29.189819"
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
|
||||
<rect
|
||||
id="rect2997-9"
|
||||
y="678.34576"
|
||||
x="106.75813"
|
||||
height="26.258072"
|
||||
width="29.724136"
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
|
||||
<rect
|
||||
y="573.46503"
|
||||
x="332.31235"
|
||||
height="104.27072"
|
||||
width="3.2554622"
|
||||
id="rect3818-4"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-18.270676,561.99481)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-8"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-19.806206,411.67121)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-8"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,94.517962,562.09633)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-0-2"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3852-4"
|
||||
d="m 406.87783,515.90696 0,70.69145 -45.58895,-32.13462 z"
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#ffffff;stroke:#ffffff;stroke-width:32.11899948;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect3088"
|
||||
width="188.24272"
|
||||
height="117.84301"
|
||||
x="578.56567"
|
||||
y="534.50873" />
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:11.66586208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 667.1767,647.76042 746.75901,734.99486 725.333,643.16913 z"
|
||||
id="path3894"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:28.84111404;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect3088-5"
|
||||
width="169.03172"
|
||||
height="105.81661"
|
||||
x="595.6264"
|
||||
y="533.38885" />
|
||||
<path
|
||||
id="path2991-7-7"
|
||||
transform="matrix(0.81013086,0,0,0.81013086,-79.003905,648.21364)"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
id="path2993-4-1"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.56153831,0,0,0.56153831,-15.312437,720.57846)" />
|
||||
<path
|
||||
id="path2991-7-1"
|
||||
transform="translate(167.79377,599.09604)"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
id="path2993-4-5"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.83611704,0,0,0.83611704,210.54515,647.51817)" />
|
||||
<rect
|
||||
id="rect2995-0-2"
|
||||
y="817.18872"
|
||||
x="409.74567"
|
||||
height="99.221687"
|
||||
width="29.189819"
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
|
||||
<rect
|
||||
id="rect2997-9-7"
|
||||
y="935.98169"
|
||||
x="410.00391"
|
||||
height="26.258072"
|
||||
width="29.724136"
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 383.15829,850.33665 -64.6851,-36.2114 10.70013,55.95688 53.98497,-19.74548 z"
|
||||
id="rect4046-3"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 372.50197,843.46474 -43.65605,-24.43447 6.99871,38.15621 36.65734,-13.72174 z"
|
||||
id="rect4046"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 462.88559,934.94792 64.6851,36.21128 -10.70013,-55.95672 -53.98497,19.74544 z"
|
||||
id="rect4046-3-2"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 471.91864,943.98419 43.65605,24.43442 -6.99871,-38.15607 -36.65734,13.72165 z"
|
||||
id="rect4046-1"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path2991-7-79"
|
||||
transform="translate(439.9024,596.03518)"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
id="path2993-4-54"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.83611704,0,0,0.83611704,482.65378,644.45731)" />
|
||||
<rect
|
||||
style="fill:#ffffff;stroke:#ffffff;stroke-width:7.27556181;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect4271"
|
||||
width="55.131588"
|
||||
height="89.475853"
|
||||
x="1123.0723"
|
||||
y="8.2489862"
|
||||
transform="matrix(0.69198127,0.72191545,-0.69198127,0.72191545,0,0)" />
|
||||
<rect
|
||||
id="rect2995-0-3-3"
|
||||
y="1106.4344"
|
||||
x="-89.869194"
|
||||
height="57.711208"
|
||||
width="24.529409"
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.77681416"
|
||||
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
|
||||
<rect
|
||||
id="rect2995-0-3-2"
|
||||
y="7.221128"
|
||||
x="1139.5251"
|
||||
height="82.866272"
|
||||
width="24.378254"
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.92796957"
|
||||
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
|
||||
<rect
|
||||
id="rect2995-0-3"
|
||||
y="814.12781"
|
||||
x="681.85431"
|
||||
height="99.221687"
|
||||
width="29.189819"
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
|
||||
<rect
|
||||
id="rect2997-9-1"
|
||||
y="932.58148"
|
||||
x="682.54327"
|
||||
height="26.258072"
|
||||
width="29.724136"
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
|
||||
<rect
|
||||
y="1088.6628"
|
||||
x="76.264809"
|
||||
height="104.27072"
|
||||
width="3.2554622"
|
||||
id="rect3818-4-8"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-274.3181,1077.1951)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-8-7"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-275.85363,926.87175)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-8-4"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-161.78913,1021.9512)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-8-7-7"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<rect
|
||||
y="1087.278"
|
||||
x="304.77451"
|
||||
height="104.27072"
|
||||
width="3.2554622"
|
||||
id="rect3818-4-8-4"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-45.808546,1075.8101)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-4-8-7-8"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,-47.344075,925.48675)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-8-4-8"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(1.0049237,0,0,0.61497516,53.509086,972.5163)"
|
||||
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
|
||||
sodipodi:ry="35.140915"
|
||||
sodipodi:rx="21.718279"
|
||||
sodipodi:cy="230.89374"
|
||||
sodipodi:cx="351.02802"
|
||||
id="path3795-8-4-8-2"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:6.68107271;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path3992-4"
|
||||
sodipodi:cx="490.42908"
|
||||
sodipodi:cy="950.84186"
|
||||
sodipodi:rx="18.487062"
|
||||
sodipodi:ry="26.506598"
|
||||
d="m 508.91614,950.84186 c 0,14.63919 -8.27694,26.5066 -18.48706,26.5066 -10.21013,0 -18.48707,-11.86741 -18.48707,-26.5066 0,-14.63919 8.27694,-26.5066 18.48707,-26.5066 10.21012,0 18.48706,11.86741 18.48706,26.5066 z"
|
||||
transform="matrix(4.8923198,0,0,1.0737805,-1482.0573,-466.94845)" />
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:12.98546886;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 967.57233,525.26244 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19027,0 0,-27.1288 29.354,0 0,-41.2377 -29.354,0 0,-30.6797 -41.19027,0 z"
|
||||
id="rect2995-0-2-7"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path2991-7-2"
|
||||
transform="translate(717.27126,597.74227)"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
id="path2993-4-7"
|
||||
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
|
||||
sodipodi:ry="104.28571"
|
||||
sodipodi:rx="104.28571"
|
||||
sodipodi:cy="290.93362"
|
||||
sodipodi:cx="255.71428"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.7638244,0,0,0.7638244,777.85958,666.54744)" />
|
||||
<rect
|
||||
id="rect2995-0-6"
|
||||
y="-220.76018"
|
||||
x="1298.3352"
|
||||
height="189.71017"
|
||||
width="28.775486"
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.52545774"
|
||||
transform="matrix(0.67068946,0.74173826,-0.74173826,0.67068946,0,0)" />
|
||||
<g
|
||||
id="g4284"
|
||||
transform="translate(-77.916708,-8.657412)">
|
||||
<path
|
||||
sodipodi:nodetypes="czcczcc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="rect4201"
|
||||
d="m 568.37427,1080.8464 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43273,8.6574 40.43273,8.6574 l 0,141.4674 c 0,0 -20.97035,-7.7215 -40.43273,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:14.36538029;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4" />
|
||||
<rect
|
||||
y="1108.1473"
|
||||
x="597.4068"
|
||||
height="5.4857273"
|
||||
width="55.265846"
|
||||
id="rect4203"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<rect
|
||||
y="1142.7776"
|
||||
x="598.48895"
|
||||
height="5.4857273"
|
||||
width="55.26585"
|
||||
id="rect4203-2"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<rect
|
||||
y="1176.1093"
|
||||
x="598.48895"
|
||||
height="5.4857273"
|
||||
width="55.26585"
|
||||
id="rect4203-2-3"
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
sodipodi:nodetypes="czc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4245"
|
||||
d="m 563.55369,1233.6274 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29117,14.7566 46.29117,14.7566"
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.6372261;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
<g
|
||||
transform="matrix(-1.0032405,0,0,1,1329.8708,99.560238)"
|
||||
id="g4277">
|
||||
<path
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:14.36538124;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4"
|
||||
d="m 519.67634,980.83663 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43272,8.6574 40.43272,8.6574 l 0,141.46737 c 0,0 -20.97034,-7.7215 -40.43272,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
|
||||
id="rect4201-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="czcczcc" />
|
||||
<rect
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect4203-21"
|
||||
width="55.26585"
|
||||
height="5.4857273"
|
||||
x="548.70886"
|
||||
y="1008.1376" />
|
||||
<rect
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect4203-2-6"
|
||||
width="55.26585"
|
||||
height="5.4857273"
|
||||
x="549.79102"
|
||||
y="1042.7678" />
|
||||
<rect
|
||||
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect4203-2-3-8"
|
||||
width="55.26585"
|
||||
height="5.4857273"
|
||||
x="549.79102"
|
||||
y="1076.0995" />
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.6372261;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="m 514.85576,1133.6176 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29116,14.7566 46.29116,14.7566"
|
||||
id="path4245-5"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="czc" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g3107"
|
||||
transform="matrix(0.53086704,-0.53086704,0.53086704,0.53086704,-205.0028,934.47839)">
|
||||
<rect
|
||||
y="1165.7029"
|
||||
x="793.91357"
|
||||
height="177.36816"
|
||||
width="131.91675"
|
||||
id="rect3075"
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.58793259;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<rect
|
||||
transform="matrix(0.69911762,0.71500668,-0.71500668,0.69911762,0,0)"
|
||||
y="145.59781"
|
||||
x="1379.6274"
|
||||
height="95.711494"
|
||||
width="95.711456"
|
||||
id="rect3075-1"
|
||||
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:12.25645447;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
transform="matrix(1.5150471,0,0,1.5150471,-201.2129,-64.133761)"
|
||||
d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
|
||||
sodipodi:ry="10"
|
||||
sodipodi:rx="10"
|
||||
sodipodi:cy="812.36218"
|
||||
sodipodi:cx="700"
|
||||
id="path3100"
|
||||
style="fill:#ffffff;stroke:#ffffff;stroke-width:12.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:type="arc" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:12.98546886;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
|
||||
id="rect2995-0-2-7-7"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
@@ -2,6 +2,7 @@ import sbt._
|
||||
import Keys._
|
||||
import org.scalatra.sbt._
|
||||
import org.scalatra.sbt.PluginKeys._
|
||||
import sbt.ScalaVersion
|
||||
import twirl.sbt.TwirlPlugin._
|
||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
|
||||
|
||||
@@ -10,7 +11,7 @@ object MyBuild extends Build {
|
||||
val Name = "gitbucket"
|
||||
val Version = "0.0.1"
|
||||
val ScalaVersion = "2.10.1"
|
||||
val ScalatraVersion = "2.2.0"
|
||||
val ScalatraVersion = "2.2.1"
|
||||
|
||||
lazy val project = Project (
|
||||
"gitbucket",
|
||||
@@ -20,18 +21,23 @@ object MyBuild extends Build {
|
||||
name := Name,
|
||||
version := Version,
|
||||
scalaVersion := ScalaVersion,
|
||||
resolvers += Classpaths.typesafeReleases,
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
|
||||
),
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
|
||||
"org.apache.commons" % "commons-io" % "1.3.2",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.2.4",
|
||||
"jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"org.pegdown" % "pegdown" % "1.3.0",
|
||||
"org.apache.commons" % "commons-compress" % "1.5",
|
||||
"org.apache.commons" % "commons-email" % "1.3.1",
|
||||
"com.typesafe.slick" %% "slick" % "1.0.1",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.3.171",
|
||||
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.2")
|
||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
|
||||
|
||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0")
|
||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
|
||||
|
||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.2.0")
|
||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
|
||||
|
||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
|
||||
|
||||
1
sbt.sh
Executable file
1
sbt.sh
Executable file
@@ -0,0 +1 @@
|
||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
|
||||
BIN
src/main/resources/noimage.png
Normal file
BIN
src/main/resources/noimage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
24
src/main/resources/update/1_2.sql
Normal file
24
src/main/resources/update/1_2.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE ACTIVITY(
|
||||
ACTIVITY_ID INT AUTO_INCREMENT,
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ACTIVITY_USER_NAME VARCHAR(100) NOT NULL,
|
||||
ACTIVITY_TYPE VARCHAR(100) NOT NULL,
|
||||
MESSAGE TEXT NOT NULL,
|
||||
ADDITIONAL_INFO TEXT,
|
||||
ACTIVITY_DATE TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE COMMIT_LOG (
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
COMMIT_ID VARCHAR(40) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_PK PRIMARY KEY (ACTIVITY_ID);
|
||||
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK1 FOREIGN KEY (ACTIVITY_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
|
||||
ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COMMIT_ID);
|
||||
ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
|
||||
8
src/main/resources/update/1_3.sql
Normal file
8
src/main/resources/update/1_3.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE ACCOUNT ADD COLUMN IMAGE VARCHAR(100);
|
||||
|
||||
UPDATE ISSUE_COMMENT SET ACTION = 'comment' WHERE ACTION IS NULL;
|
||||
|
||||
ALTER TABLE ISSUE_COMMENT ALTER COLUMN ACTION VARCHAR(20) NOT NULL;
|
||||
|
||||
UPDATE ISSUE_COMMENT SET ACTION = 'close_comment' WHERE ACTION = 'close';
|
||||
UPDATE ISSUE_COMMENT SET ACTION = 'reopen_comment' WHERE ACTION = 'reopen';
|
||||
24
src/main/resources/update/1_4.sql
Normal file
24
src/main/resources/update/1_4.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE GROUP_MEMBER(
|
||||
GROUP_NAME VARCHAR(100) NOT NULL,
|
||||
USER_NAME VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
|
||||
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
|
||||
ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||
SELECT
|
||||
A.USER_NAME,
|
||||
A.REPOSITORY_NAME,
|
||||
A.ISSUE_ID,
|
||||
NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
||||
FROM ISSUE A
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) B
|
||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);
|
||||
21
src/main/resources/update/1_5.sql
Normal file
21
src/main/resources/update/1_5.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100);
|
||||
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100);
|
||||
ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100);
|
||||
ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100);
|
||||
|
||||
CREATE TABLE PULL_REQUEST(
|
||||
USER_NAME VARCHAR(100) NOT NULL,
|
||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
ISSUE_ID INT NOT NULL,
|
||||
BRANCH VARCHAR(100) NOT NULL,
|
||||
REQUEST_USER_NAME VARCHAR(100) NOT NULL,
|
||||
REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||
REQUEST_BRANCH VARCHAR(100) NOT NULL,
|
||||
COMMIT_ID_FROM VARCHAR(40) NOT NULL,
|
||||
COMMIT_ID_TO VARCHAR(40) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
||||
|
||||
ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -5,7 +5,10 @@ import javax.servlet._
|
||||
class ScalatraBootstrap extends LifeCycle {
|
||||
override def init(context: ServletContext) {
|
||||
context.mount(new IndexController, "/")
|
||||
context.mount(new SearchController, "/")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
context.mount(new SignInController, "/*")
|
||||
context.mount(new DashboardController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
context.mount(new CreateRepositoryController, "/*")
|
||||
@@ -15,7 +18,8 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.mount(new LabelsController, "/*")
|
||||
context.mount(new MilestonesController, "/*")
|
||||
context.mount(new IssuesController, "/*")
|
||||
context.mount(new SettingsController, "/*")
|
||||
context.mount(new PullRequestsController, "/*")
|
||||
context.mount(new RepositorySettingsController, "/*")
|
||||
|
||||
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
|
||||
if(!dir.exists){
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.OneselfAuthenticator
|
||||
import util.{FileUtil, OneselfAuthenticator}
|
||||
import util.StringUtil._
|
||||
import util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.FlashMapSupport
|
||||
|
||||
class AccountController extends AccountControllerBase
|
||||
with SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator
|
||||
with SystemSettingsService with AccountService with RepositoryService with ActivityService
|
||||
with OneselfAuthenticator
|
||||
|
||||
trait AccountControllerBase extends ControllerBase {
|
||||
self: SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator =>
|
||||
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
|
||||
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
|
||||
with OneselfAuthenticator =>
|
||||
|
||||
case class AccountNewForm(userName: String, password: String,mailAddress: String, url: Option[String])
|
||||
case class AccountNewForm(userName: String, password: String,mailAddress: String,
|
||||
url: Option[String], fileId: Option[String])
|
||||
|
||||
case class AccountEditForm(password: Option[String], mailAddress: String, url: Option[String])
|
||||
case class AccountEditForm(password: Option[String], mailAddress: String,
|
||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
|
||||
val newForm = mapping(
|
||||
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text())))
|
||||
)(AccountNewForm.apply)
|
||||
|
||||
val editForm = mapping(
|
||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
)(AccountEditForm.apply)
|
||||
|
||||
/**
|
||||
@@ -33,52 +42,74 @@ trait AccountControllerBase extends ControllerBase {
|
||||
*/
|
||||
get("/:userName") {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map {
|
||||
account.html.info(_, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
|
||||
getAccountByUserName(userName).map { account =>
|
||||
params.getOrElse("tab", "repositories") match {
|
||||
// Public Activity
|
||||
case "activity" =>
|
||||
_root_.account.html.activity(account,
|
||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||
getActivitiesByUser(userName, true))
|
||||
|
||||
// Members
|
||||
case "members" if(account.isGroupAccount) =>
|
||||
_root_.account.html.members(account, getGroupMembers(account.userName))
|
||||
|
||||
// Repositories
|
||||
case _ =>
|
||||
_root_.account.html.repositories(account,
|
||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
|
||||
get("/:userName/_avatar"){
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
||||
contentType = FileUtil.getMimeType(image)
|
||||
new java.io.File(getUserUploadDir(userName), image)
|
||||
} getOrElse {
|
||||
contentType = "image/png"
|
||||
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
|
||||
}
|
||||
}
|
||||
|
||||
get("/:userName/_edit")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map(x => account.html.edit(Some(x))) getOrElse NotFound
|
||||
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:userName/_edit", editForm)(oneselfOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { account =>
|
||||
updateAccount(account.copy(
|
||||
password = form.password.map(encrypt).getOrElse(account.password),
|
||||
password = form.password.map(sha1).getOrElse(account.password),
|
||||
mailAddress = form.mailAddress,
|
||||
url = form.url))
|
||||
redirect("/%s".format(userName))
|
||||
|
||||
updateImage(userName, form.fileId, form.clearImage)
|
||||
flash += "info" -> "Account information has been updated."
|
||||
redirect(s"/${userName}/_edit")
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/register"){
|
||||
if(loadSystemSettings().allowAccountRegistration){
|
||||
account.html.edit(None)
|
||||
if(context.loginAccount.isDefined){
|
||||
redirect("/")
|
||||
} else {
|
||||
account.html.edit(None, None)
|
||||
}
|
||||
} else NotFound
|
||||
}
|
||||
|
||||
post("/register", newForm){ newForm =>
|
||||
post("/register", newForm){ form =>
|
||||
if(loadSystemSettings().allowAccountRegistration){
|
||||
createAccount(newForm.userName, encrypt(newForm.password), newForm.mailAddress, false, newForm.url)
|
||||
createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/signin")
|
||||
} else NotFound
|
||||
}
|
||||
|
||||
// TODO Merge with UserManagementController
|
||||
private def uniqueUserName: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getAccountByUserName(value).map { _ => "User already exists." }
|
||||
}
|
||||
|
||||
// TODO Merge with UserManagementController
|
||||
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getAccountByMailAddress(value)
|
||||
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
||||
.map { _ => "Mail address is already registered." }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,24 +1,61 @@
|
||||
package app
|
||||
|
||||
import model.Account
|
||||
import util.Validations
|
||||
import _root_.util.Directory._
|
||||
import _root_.util.{FileUtil, Validations}
|
||||
import org.scalatra._
|
||||
import org.scalatra.json._
|
||||
import org.json4s._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import model.Account
|
||||
import scala.Some
|
||||
import service.AccountService
|
||||
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||
|
||||
/**
|
||||
* Provides generic features for ScalatraServlet implementations.
|
||||
* Provides generic features for controller implementations.
|
||||
*/
|
||||
abstract class ControllerBase extends ScalatraFilter
|
||||
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
|
||||
|
||||
implicit val jsonFormats = DefaultFormats
|
||||
|
||||
// Don't set content type via Accept header.
|
||||
override def format(implicit request: HttpServletRequest) = ""
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val httpResponse = response.asInstanceOf[HttpServletResponse]
|
||||
val context = request.getServletContext.getContextPath
|
||||
val path = httpRequest.getRequestURI.substring(context.length)
|
||||
|
||||
if(path.startsWith("/console/")){
|
||||
val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]
|
||||
if(account == null){
|
||||
// Redirect to login form
|
||||
httpResponse.sendRedirect(context + "/signin?" + path)
|
||||
} else if(account.isAdmin){
|
||||
// H2 Console (administrators only)
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// Redirect to dashboard
|
||||
httpResponse.sendRedirect(context + "/")
|
||||
}
|
||||
} else if(path.startsWith("/git/")){
|
||||
// Git repository
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// Scalatra actions
|
||||
super.doFilter(request, response, chain)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context object for the request.
|
||||
*/
|
||||
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL)
|
||||
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
|
||||
|
||||
private def currentURL: String = {
|
||||
val queryString = request.getQueryString
|
||||
@@ -73,7 +110,11 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
if(context.loginAccount.isDefined){
|
||||
org.scalatra.Unauthorized(redirect("/"))
|
||||
} else {
|
||||
org.scalatra.Unauthorized(redirect("/signin?" + currentURL))
|
||||
if(request.getMethod.toUpperCase == "POST"){
|
||||
org.scalatra.Unauthorized(redirect("/signin"))
|
||||
} else {
|
||||
org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
org.scalatra.Unauthorized()
|
||||
@@ -87,4 +128,99 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
|
||||
}
|
||||
|
||||
case class Context(path: String, loginAccount: Option[Account], currentUrl: String)
|
||||
/**
|
||||
* Context object for the current request.
|
||||
*/
|
||||
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
|
||||
|
||||
def redirectUrl = {
|
||||
if(request.getParameter("redirect") != null){
|
||||
request.getParameter("redirect")
|
||||
} else {
|
||||
currentUrl
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object from cache.
|
||||
*
|
||||
* If object has not been cached with the specified key then retrieves by given action.
|
||||
* Cached object are available during a request.
|
||||
*/
|
||||
def cache[A](key: String)(action: => A): A = {
|
||||
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse {
|
||||
val newObject = action
|
||||
request.setAttribute("cache." + key, newObject)
|
||||
newObject
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base trait for controllers which manages account information.
|
||||
*/
|
||||
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
|
||||
self: AccountService =>
|
||||
|
||||
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
|
||||
if(clearImage){
|
||||
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
||||
new java.io.File(getUserUploadDir(userName), image).delete()
|
||||
updateAvatarImage(userName, None)
|
||||
}
|
||||
} else {
|
||||
fileId.map { fileId =>
|
||||
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
|
||||
FileUtils.moveFile(
|
||||
getTemporaryFile(fileId),
|
||||
new java.io.File(getUserUploadDir(userName), filename)
|
||||
)
|
||||
updateAvatarImage(userName, Some(filename))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected def uniqueUserName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
getAccountByUserName(value).map { _ => "User already exists." }
|
||||
}
|
||||
|
||||
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
getAccountByMailAddress(value)
|
||||
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
||||
.map { _ => "Mail address is already registered." }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base trait for controllers which needs file uploading feature.
|
||||
*/
|
||||
trait FileUploadControllerBase {
|
||||
|
||||
def generateFileId: String =
|
||||
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
|
||||
|
||||
def TemporaryDir(implicit session: HttpSession): java.io.File =
|
||||
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
|
||||
|
||||
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
|
||||
new java.io.File(TemporaryDir, fileId)
|
||||
|
||||
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
|
||||
// getTemporaryFile(fileId).delete()
|
||||
|
||||
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
|
||||
FileUtils.deleteDirectory(TemporaryDir)
|
||||
|
||||
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
|
||||
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
|
||||
if(filename.isDefined){
|
||||
session.removeAttribute("upload_" + fileId)
|
||||
}
|
||||
filename
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,99 +1,197 @@
|
||||
package app
|
||||
|
||||
import util.Directory._
|
||||
import util.UsersAuthenticator
|
||||
import util._
|
||||
import service._
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.apache.commons.io._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import scala.Some
|
||||
|
||||
class CreateRepositoryController extends CreateRepositoryControllerBase
|
||||
with RepositoryService with AccountService with WikiService with LabelsService with UsersAuthenticator
|
||||
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
||||
with UsersAuthenticator with ReadableUsersAuthenticator
|
||||
|
||||
/**
|
||||
* Creates new repository.
|
||||
*/
|
||||
trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
self: RepositoryService with WikiService with LabelsService with UsersAuthenticator =>
|
||||
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
||||
with UsersAuthenticator with ReadableUsersAuthenticator =>
|
||||
|
||||
case class RepositoryCreationForm(name: String, description: Option[String])
|
||||
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
||||
|
||||
val form = mapping(
|
||||
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
|
||||
"description" -> trim(label("Description" , optional(text())))
|
||||
case class ForkRepositoryForm(owner: String, name: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
|
||||
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean())),
|
||||
"createReadme" -> trim(label("Create README" , boolean()))
|
||||
)(RepositoryCreationForm.apply)
|
||||
|
||||
val forkForm = mapping(
|
||||
"owner" -> trim(label("Repository owner", text(required))),
|
||||
"name" -> trim(label("Repository name", text(required)))
|
||||
)(ForkRepositoryForm.apply)
|
||||
|
||||
/**
|
||||
* Show the new repository form.
|
||||
*/
|
||||
get("/new")(usersOnly {
|
||||
html.newrepo()
|
||||
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
|
||||
})
|
||||
|
||||
/**
|
||||
* Create new repository.
|
||||
*/
|
||||
post("/new", form)(usersOnly { form =>
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
post("/new", newForm)(usersOnly { form =>
|
||||
LockUtil.lock(s"${form.owner}/${form.name}/create"){
|
||||
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
|
||||
val ownerAccount = getAccountByUserName(form.owner).get
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
// Insert to the database at first
|
||||
createRepository(form.name, loginUserName, form.description)
|
||||
// Insert to the database at first
|
||||
createRepository(form.name, form.owner, form.description, form.isPrivate)
|
||||
|
||||
// Insert default labels
|
||||
createLabel(loginUserName, form.name, "bug", "fc2929")
|
||||
createLabel(loginUserName, form.name, "duplicate", "cccccc")
|
||||
createLabel(loginUserName, form.name, "enhancement", "84b6eb")
|
||||
createLabel(loginUserName, form.name, "invalid", "e6e6e6")
|
||||
createLabel(loginUserName, form.name, "question", "cc317c")
|
||||
createLabel(loginUserName, form.name, "wontfix", "ffffff")
|
||||
// Add collaborators for group repository
|
||||
if(ownerAccount.isGroupAccount){
|
||||
getGroupMembers(form.owner).foreach { userName =>
|
||||
addCollaborator(form.owner, form.name, userName)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(loginUserName, form.name)
|
||||
val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build
|
||||
// Insert default labels
|
||||
insertDefaultLabels(form.owner, form.name)
|
||||
|
||||
repository.create
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(form.owner, form.name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
val config = repository.getConfig
|
||||
config.setBoolean("http", null, "receivepack", true)
|
||||
config.save
|
||||
if(form.createReadme){
|
||||
val tmpdir = getInitRepositoryDir(form.owner, form.name)
|
||||
try {
|
||||
// Clone the repository
|
||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
||||
|
||||
val tmpdir = getInitRepositoryDir(loginUserName, form.name)
|
||||
try {
|
||||
// Clone the repository
|
||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
||||
|
||||
// Create README.md
|
||||
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
|
||||
if(form.description.nonEmpty){
|
||||
form.name + "\n===============\n\n" + form.description.get
|
||||
} else {
|
||||
form.name + "\n===============\n"
|
||||
}, "UTF-8")
|
||||
|
||||
val git = Git.open(tmpdir)
|
||||
git.add.addFilepattern("README.md").call
|
||||
git.commit.setMessage("Initial commit").call
|
||||
git.push.call
|
||||
|
||||
} finally {
|
||||
FileUtils.deleteDirectory(tmpdir)
|
||||
// Create README.md
|
||||
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
|
||||
if(form.description.nonEmpty){
|
||||
form.name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
form.description.get
|
||||
} else {
|
||||
form.name + "\n" +
|
||||
"===============\n"
|
||||
}, "UTF-8")
|
||||
|
||||
val git = Git.open(tmpdir)
|
||||
git.add.addFilepattern("README.md").call
|
||||
git.commit
|
||||
.setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress))
|
||||
.setMessage("Initial commit").call
|
||||
git.push.call
|
||||
|
||||
} finally {
|
||||
FileUtils.deleteDirectory(tmpdir)
|
||||
}
|
||||
}
|
||||
|
||||
// Create Wiki repository
|
||||
createWikiRepository(loginAccount, form.owner, form.name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
|
||||
}
|
||||
|
||||
// redirect to the repository
|
||||
redirect(s"/${form.owner}/${form.name}")
|
||||
}
|
||||
|
||||
// Create Wiki repository
|
||||
createWikiRepository(context.loginAccount.get, form.name)
|
||||
|
||||
// redirect to the repository
|
||||
redirect("/%s/%s".format(loginUserName, form.name))
|
||||
})
|
||||
|
||||
|
||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
|
||||
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
|
||||
// Insert to the database at first
|
||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||
|
||||
createRepository(
|
||||
repositoryName = repository.name,
|
||||
userName = loginUserName,
|
||||
description = repository.repository.description,
|
||||
isPrivate = repository.repository.isPrivate,
|
||||
originRepositoryName = Some(originRepositoryName),
|
||||
originUserName = Some(originUserName),
|
||||
parentRepositoryName = Some(repository.name),
|
||||
parentUserName = Some(repository.owner)
|
||||
)
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(loginUserName, repository.name)
|
||||
|
||||
// clone repository actually
|
||||
JGitUtil.cloneRepository(
|
||||
getRepositoryDir(repository.owner, repository.name),
|
||||
getRepositoryDir(loginUserName, repository.name))
|
||||
|
||||
// Create Wiki repository
|
||||
JGitUtil.cloneRepository(
|
||||
getWikiRepositoryDir(repository.owner, repository.name),
|
||||
getWikiRepositoryDir(loginUserName, repository.name))
|
||||
|
||||
// insert commit id
|
||||
JGitUtil.withGit(getRepositoryDir(loginUserName, repository.name)){ git =>
|
||||
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
|
||||
JGitUtil.getCommitLog(git, branch) match {
|
||||
case Right((commits, _)) => commits.foreach { commit =>
|
||||
if(!existsCommitId(loginUserName, repository.name, commit.id)){
|
||||
insertCommitId(loginUserName, repository.name, commit.id)
|
||||
}
|
||||
}
|
||||
case Left(_) => ???
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record activity
|
||||
recordForkActivity(repository.owner, repository.name, loginUserName)
|
||||
}
|
||||
// redirect to the repository
|
||||
redirect("/%s/%s".format(loginUserName, repository.name))
|
||||
}
|
||||
})
|
||||
|
||||
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
||||
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
||||
createLabel(userName, repositoryName, "enhancement", "84b6eb")
|
||||
createLabel(userName, repositoryName, "invalid", "e6e6e6")
|
||||
createLabel(userName, repositoryName, "question", "cc317c")
|
||||
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
||||
}
|
||||
|
||||
private def existsAccount: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate check for the repository name.
|
||||
*/
|
||||
private def unique: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
params.get("owner").flatMap { userName =>
|
||||
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
119
src/main/scala/app/DashboardController.scala
Normal file
119
src/main/scala/app/DashboardController.scala
Normal file
@@ -0,0 +1,119 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.UsersAuthenticator
|
||||
|
||||
class DashboardController extends DashboardControllerBase
|
||||
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||
with UsersAuthenticator
|
||||
|
||||
trait DashboardControllerBase extends ControllerBase {
|
||||
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
|
||||
|
||||
get("/dashboard/issues/repos")(usersOnly {
|
||||
searchIssues("all")
|
||||
})
|
||||
|
||||
get("/dashboard/issues/assigned")(usersOnly {
|
||||
searchIssues("assigned")
|
||||
})
|
||||
|
||||
get("/dashboard/issues/created_by")(usersOnly {
|
||||
searchIssues("created_by")
|
||||
})
|
||||
|
||||
get("/dashboard/pulls")(usersOnly {
|
||||
searchPullRequests("created_by", None)
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/owned")(usersOnly {
|
||||
searchPullRequests("created_by", None)
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/public")(usersOnly {
|
||||
searchPullRequests("not_created_by", None)
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
|
||||
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
|
||||
})
|
||||
|
||||
private def searchIssues(filter: String) = {
|
||||
import IssuesService._
|
||||
|
||||
// condition
|
||||
val sessionKey = "dashboard/issues"
|
||||
val condition = if(request.getQueryString == null)
|
||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
||||
else IssueSearchCondition(request)
|
||||
|
||||
session.put(sessionKey, condition)
|
||||
|
||||
val userName = context.loginAccount.get.userName
|
||||
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
||||
val filterUser = Map(filter -> userName)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
//
|
||||
dashboard.html.issues(
|
||||
issues.html.listparts(
|
||||
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
|
||||
condition),
|
||||
countIssue(condition, Map.empty, false, repositories: _*),
|
||||
countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
|
||||
countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
|
||||
countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
|
||||
condition,
|
||||
filter)
|
||||
|
||||
}
|
||||
|
||||
private def searchPullRequests(filter: String, repository: Option[String]) = {
|
||||
import IssuesService._
|
||||
import PullRequestService._
|
||||
|
||||
// condition
|
||||
val sessionKey = "dashboard/pulls"
|
||||
val condition = {
|
||||
if(request.getQueryString == null)
|
||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
||||
else
|
||||
IssueSearchCondition(request)
|
||||
}.copy(repo = repository)
|
||||
|
||||
session.put(sessionKey, condition)
|
||||
|
||||
val userName = context.loginAccount.get.userName
|
||||
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
|
||||
val filterUser = Map(filter -> userName)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
val counts = countIssueGroupByRepository(
|
||||
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
|
||||
|
||||
getRepositoryNamesOfUser(userName).map { repoName =>
|
||||
(userName, repoName, counts.collectFirst { case (_, repoName, count) => count })
|
||||
}
|
||||
|
||||
dashboard.html.pulls(
|
||||
pulls.html.listparts(
|
||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
|
||||
condition,
|
||||
None,
|
||||
false),
|
||||
getPullRequestCountGroupByUser(condition.state == "closed", userName, None),
|
||||
getRepositoryNamesOfUser(userName).map { RepoName =>
|
||||
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
|
||||
}.sortBy(_._3).reverse,
|
||||
condition,
|
||||
filter)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
32
src/main/scala/app/FileUploadController.scala
Normal file
32
src/main/scala/app/FileUploadController.scala
Normal file
@@ -0,0 +1,32 @@
|
||||
package app
|
||||
|
||||
import util.{FileUtil}
|
||||
import org.scalatra._
|
||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
/**
|
||||
* Provides Ajax based file upload functionality.
|
||||
*
|
||||
* This servlet saves uploaded file as temporary file and returns the unique id.
|
||||
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
|
||||
*/
|
||||
class FileUploadController extends ScalatraServlet
|
||||
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
|
||||
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||
|
||||
post("/image"){
|
||||
fileParams.get("file") match {
|
||||
case Some(file) if(FileUtil.isImage(file.name)) => {
|
||||
val fileId = generateFileId
|
||||
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
|
||||
session += "upload_" + fileId -> file.name
|
||||
Ok(fileId)
|
||||
}
|
||||
case None => BadRequest
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class IndexController extends IndexControllerBase with RepositoryService with AccountService with SystemSettingsService
|
||||
class IndexController extends IndexControllerBase
|
||||
with RepositoryService with SystemSettingsService with ActivityService with AccountService
|
||||
with UsersAuthenticator
|
||||
|
||||
trait IndexControllerBase extends ControllerBase {
|
||||
self: RepositoryService with SystemSettingsService with ActivityService with AccountService
|
||||
with UsersAuthenticator =>
|
||||
|
||||
trait IndexControllerBase extends ControllerBase { self: RepositoryService with SystemSettingsService =>
|
||||
|
||||
get("/"){
|
||||
html.index(getAccessibleRepositories(context.loginAccount, baseUrl), loadSystemSettings(),
|
||||
context.loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil))
|
||||
val loginAccount = context.loginAccount
|
||||
|
||||
html.index(getRecentActivities(),
|
||||
getVisibleRepositories(loginAccount, baseUrl),
|
||||
loadSystemSettings(),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* JSON API for collaborator completion.
|
||||
*
|
||||
* TODO Move to other controller?
|
||||
*/
|
||||
get("/_user/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,23 +4,22 @@ import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import IssuesService._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator}
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier}
|
||||
import org.scalatra.Ok
|
||||
|
||||
class IssuesController extends IssuesControllerBase
|
||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService
|
||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
trait IssuesControllerBase extends ControllerBase {
|
||||
self: IssuesService with RepositoryService with LabelsService with MilestonesService
|
||||
self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
case class IssueCreateForm(title: String, content: Option[String],
|
||||
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
|
||||
|
||||
case class IssueEditForm(title: String, content: Option[String])
|
||||
|
||||
case class CommentForm(issueId: Int, content: String)
|
||||
case class IssueStateForm(issueId: Int, content: Option[String])
|
||||
|
||||
val issueCreateForm = mapping(
|
||||
"title" -> trim(label("Title", text(required))),
|
||||
@@ -40,6 +39,11 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
"content" -> trim(label("Comment", text(required)))
|
||||
)(CommentForm.apply)
|
||||
|
||||
val issueStateForm = mapping(
|
||||
"issueId" -> label("Issue Id", number()),
|
||||
"content" -> trim(optional(text()))
|
||||
)(IssueStateForm.apply)
|
||||
|
||||
get("/:owner/:repository/issues")(referrersOnly {
|
||||
searchIssues("all", _)
|
||||
})
|
||||
@@ -63,7 +67,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getComments(owner, name, issueId.toInt),
|
||||
getIssueLabels(owner, name, issueId.toInt),
|
||||
(getCollaborators(owner, name) :+ owner).sorted,
|
||||
getMilestones(owner, name),
|
||||
getMilestonesWithIssueCount(owner, name),
|
||||
getLabels(owner, name),
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
repository)
|
||||
@@ -86,11 +90,14 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val writable = hasWritePermission(owner, name, context.loginAccount)
|
||||
val userName = context.loginAccount.get.userName
|
||||
|
||||
val issueId = createIssue(owner, name, context.loginAccount.get.userName, form.title, form.content,
|
||||
// insert issue
|
||||
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
||||
if(writable) form.assignedUserName else None,
|
||||
if(writable) form.milestoneId else None)
|
||||
|
||||
// insert labels
|
||||
if(writable){
|
||||
form.labelNames.map { value =>
|
||||
val labels = getLabels(owner, name)
|
||||
@@ -102,7 +109,15 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
redirect("/%s/%s/issues/%d".format(owner, name, issueId))
|
||||
// record activity
|
||||
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
||||
@@ -112,28 +127,23 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getIssue(owner, name, params("id")).map { issue =>
|
||||
if(isEditable(owner, name, issue.openedUserName)){
|
||||
updateIssue(owner, name, issue.issueId, form.title, form.content)
|
||||
redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId))
|
||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
getIssue(owner, name, form.issueId.toString).map { issue =>
|
||||
redirect("/%s/%s/issues/%d#comment-%d".format(
|
||||
owner, name, form.issueId,
|
||||
createComment(owner, name, context.loginAccount.get.userName,
|
||||
form.issueId,
|
||||
form.content,
|
||||
if(isEditable(owner, name, issue.openedUserName)){
|
||||
params.get("action") filter { action =>
|
||||
updateClosed(owner, name, form.issueId, if(action == "close") true else false) > 0
|
||||
}
|
||||
} else None)
|
||||
))
|
||||
}
|
||||
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
||||
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
@@ -143,7 +153,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getComment(owner, name, params("id")).map { comment =>
|
||||
if(isEditable(owner, name, comment.commentedUserName)){
|
||||
updateComment(comment.commentId, form.content)
|
||||
redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId))
|
||||
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
})
|
||||
@@ -159,7 +169,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("title" -> x.title,
|
||||
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
|
||||
repository, false, true, true)
|
||||
repository, false, true)
|
||||
))
|
||||
}
|
||||
} else Unauthorized
|
||||
@@ -176,7 +186,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("content" -> view.Markdown.toHtml(x.content,
|
||||
repository, false, true, true)
|
||||
repository, false, true)
|
||||
))
|
||||
}
|
||||
} else Unauthorized
|
||||
@@ -198,32 +208,130 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt,
|
||||
params.get("assignedUserName") filter (_.trim != ""))
|
||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
|
||||
Ok("updated")
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
|
||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt,
|
||||
params.get("milestoneId") collect { case x if x.trim != "" => x.toInt })
|
||||
Ok("updated")
|
||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
|
||||
milestoneId("milestoneId").map { milestoneId =>
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
|
||||
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
|
||||
} getOrElse NotFound
|
||||
} getOrElse Ok()
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||
val action = params.get("value")
|
||||
|
||||
executeBatch(repository) {
|
||||
handleComment(_, None, repository)( _ => action)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
|
||||
val labelId = params("value").toInt
|
||||
|
||||
executeBatch(repository) { issueId =>
|
||||
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
|
||||
val value = assignedUserName("value")
|
||||
|
||||
executeBatch(repository) {
|
||||
updateAssignedUserName(repository.owner, repository.name, _, value)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
|
||||
val value = milestoneId("value")
|
||||
|
||||
executeBatch(repository) {
|
||||
updateMilestoneId(repository.owner, repository.name, _, value)
|
||||
}
|
||||
})
|
||||
|
||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||
val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
|
||||
|
||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
|
||||
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
|
||||
params("checked").split(',') map(_.toInt) foreach execute
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
}
|
||||
|
||||
/**
|
||||
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||
*/
|
||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
||||
(getAction: model.Issue => Option[String] =
|
||||
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val userName = context.loginAccount.get.userName
|
||||
|
||||
getIssue(owner, name, issueId.toString) map { issue =>
|
||||
val (action, recordActivity) =
|
||||
getAction(issue)
|
||||
.collect {
|
||||
case "close" => true -> (Some("close") ->
|
||||
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||
case "reopen" => false -> (Some("reopen") ->
|
||||
Some(recordReopenIssueActivity _))
|
||||
}
|
||||
.map { case (closed, t) =>
|
||||
updateClosed(owner, name, issueId, closed)
|
||||
t
|
||||
}
|
||||
.getOrElse(None -> None)
|
||||
|
||||
val commentId = content
|
||||
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
|
||||
.getOrElse ( action.get.capitalize -> action.get )
|
||||
match {
|
||||
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
||||
}
|
||||
|
||||
// record activity
|
||||
content foreach {
|
||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||
(owner, name, userName, issueId, _)
|
||||
}
|
||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
||||
|
||||
// notifications
|
||||
Notifier() match {
|
||||
case f =>
|
||||
content foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
||||
}
|
||||
}
|
||||
action foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
issue -> commentId
|
||||
}
|
||||
}
|
||||
|
||||
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
||||
val owner = repository.owner
|
||||
val repoName = repository.name
|
||||
val userName = if(filter != "all") Some(params("userName")) else None
|
||||
val sessionKey = "%s/%s/issues".format(owner, repoName)
|
||||
|
||||
val page = try {
|
||||
val i = params.getOrElse("page", "1").toInt
|
||||
if(i <= 0) 1 else i
|
||||
} catch {
|
||||
case e: NumberFormatException => 1
|
||||
}
|
||||
val filterUser = Map(filter -> params.getOrElse("userName", ""))
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val sessionKey = s"${owner}/${repoName}/issues"
|
||||
|
||||
// retrieve search condition
|
||||
val condition = if(request.getQueryString == null){
|
||||
@@ -233,16 +341,17 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
session.put(sessionKey, condition)
|
||||
|
||||
issues.html.list(
|
||||
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
|
||||
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
|
||||
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
|
||||
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
|
||||
countIssue(owner, repoName, condition, "all", None),
|
||||
context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))),
|
||||
context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))),
|
||||
countIssueGroupByLabels(owner, repoName, condition, filter, userName),
|
||||
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
|
||||
countIssue(condition, Map.empty, false, owner -> repoName),
|
||||
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
|
||||
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
|
||||
countIssueGroupByLabels(owner, repoName, condition, filterUser),
|
||||
condition,
|
||||
filter,
|
||||
repository,
|
||||
|
||||
@@ -13,18 +13,18 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
case class LabelForm(labelName: String, color: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"newLabelName" -> trim(label("Label name", text(required, identifier, maxlength(100)))),
|
||||
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||
"newColor" -> trim(label("Color", text(required, color)))
|
||||
)(LabelForm.apply)
|
||||
|
||||
val editForm = mapping(
|
||||
"editLabelName" -> trim(label("Label name", text(required, identifier, maxlength(100)))),
|
||||
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||
"editColor" -> trim(label("Color", text(required, color)))
|
||||
)(LabelForm.apply)
|
||||
|
||||
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
||||
redirect("/%s/%s/issues".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
|
||||
@@ -47,4 +47,18 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* Constraint for the identifier such as user name, repository name or page name.
|
||||
*/
|
||||
private def labelName: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
if(!value.matches("^[^,]+$")){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
Some(s"${name} starts with invalid character.")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package app
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||
|
||||
class MilestonesController extends MilestonesControllerBase
|
||||
with MilestonesService with RepositoryService with AccountService
|
||||
@@ -35,7 +35,7 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
|
||||
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
||||
createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate)
|
||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
|
||||
@@ -45,28 +45,28 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
|
||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
||||
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
|
||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
||||
closeMilestone(milestone)
|
||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
|
||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
||||
openMilestone(milestone)
|
||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
|
||||
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
|
||||
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
|
||||
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
|
||||
415
src/main/scala/app/PullRequestsController.scala
Normal file
415
src/main/scala/app/PullRequestsController.scala
Normal file
@@ -0,0 +1,415 @@
|
||||
package app
|
||||
|
||||
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier}
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import service._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import org.apache.commons.io.FileUtils
|
||||
import scala.collection.JavaConverters._
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
|
||||
import service.IssuesService._
|
||||
import service.PullRequestService._
|
||||
import util.JGitUtil.DiffInfo
|
||||
import scala.Some
|
||||
import service.RepositoryService.RepositoryTreeNode
|
||||
import util.JGitUtil.CommitInfo
|
||||
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
val pullRequestForm = mapping(
|
||||
"title" -> trim(label("Title" , text(required, maxlength(100)))),
|
||||
"content" -> trim(label("Content", optional(text()))),
|
||||
"targetUserName" -> trim(text(required, maxlength(100))),
|
||||
"targetBranch" -> trim(text(required, maxlength(100))),
|
||||
"requestUserName" -> trim(text(required, maxlength(100))),
|
||||
"requestBranch" -> trim(text(required, maxlength(100))),
|
||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||
"commitIdTo" -> trim(text(required, maxlength(40)))
|
||||
)(PullRequestForm.apply)
|
||||
|
||||
val mergeForm = mapping(
|
||||
"message" -> trim(label("Message", text(required)))
|
||||
)(MergeForm.apply)
|
||||
|
||||
case class PullRequestForm(
|
||||
title: String,
|
||||
content: Option[String],
|
||||
targetUserName: String,
|
||||
targetBranch: String,
|
||||
requestUserName: String,
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String)
|
||||
|
||||
case class MergeForm(message: String)
|
||||
|
||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||
searchPullRequests(None, repository)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
|
||||
searchPullRequests(Some(params("userName")), repository)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val issueId = params("id").toInt
|
||||
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
JGitUtil.withGit(getRepositoryDir(owner, name)){ git =>
|
||||
val requestCommitId = git.getRepository.resolve(pullreq.requestBranch)
|
||||
|
||||
val (commits, diffs) =
|
||||
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
|
||||
|
||||
pulls.html.pullreq(
|
||||
issue, pullreq,
|
||||
getComments(owner, name, issueId.toInt),
|
||||
(getCollaborators(owner, name) :+ owner).sorted,
|
||||
getMilestonesWithIssueCount(owner, name),
|
||||
commits,
|
||||
diffs,
|
||||
if(issue.closed){
|
||||
false
|
||||
} else {
|
||||
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch)
|
||||
},
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
repository,
|
||||
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
|
||||
}
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||
LockUtil.lock(s"${repository.owner}/${repository.name}/merge"){
|
||||
val issueId = params("id").toInt
|
||||
|
||||
getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) =>
|
||||
val remote = getRepositoryDir(repository.owner, repository.name)
|
||||
val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
|
||||
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
|
||||
|
||||
try {
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(repository.owner, repository.name, issueId, true)
|
||||
|
||||
// record activity
|
||||
recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// TODO apply ref comment
|
||||
|
||||
// fetch pull request to temporary working repository
|
||||
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
|
||||
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(repository.owner, repository.name).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
|
||||
|
||||
// merge pull request
|
||||
git.checkout.setName(pullreq.branch).call
|
||||
|
||||
val result = git.merge
|
||||
.include(git.getRepository.resolve(pullRequestBranchName))
|
||||
.setFastForward(FastForwardMode.NO_FF)
|
||||
.setCommit(false)
|
||||
.call
|
||||
|
||||
if(result.getConflicts != null){
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
// merge commit
|
||||
git.getRepository.writeMergeCommitMsg(
|
||||
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
|
||||
+ form.message)
|
||||
|
||||
git.commit
|
||||
.setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
|
||||
.call
|
||||
|
||||
// push
|
||||
git.push.call
|
||||
|
||||
val (commits, _) = getRequestCompareInfo(repository.owner, repository.name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
commits.flatten.foreach { commit =>
|
||||
if(!existsCommitId(repository.owner, repository.name, commit.id)){
|
||||
insertCommitId(repository.owner, repository.name, commit.id)
|
||||
}
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier.msgStatus(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
|
||||
} finally {
|
||||
git.getRepository.close
|
||||
FileUtils.deleteDirectory(tmpdir)
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging.
|
||||
* Returns true if conflict will be caused.
|
||||
*/
|
||||
private def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
||||
// TODO Are there more quick way?
|
||||
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
|
||||
val remote = getRepositoryDir(userName, repositoryName)
|
||||
val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")
|
||||
if(tmpdir.exists()){
|
||||
FileUtils.deleteDirectory(tmpdir)
|
||||
}
|
||||
|
||||
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call
|
||||
try {
|
||||
git.checkout.setName(branch).call
|
||||
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
|
||||
|
||||
val result = git.merge
|
||||
.include(git.getRepository.resolve("FETCH_HEAD"))
|
||||
.setCommit(false).call
|
||||
|
||||
result.getConflicts != null
|
||||
|
||||
} finally {
|
||||
git.getRepository.close
|
||||
FileUtils.deleteDirectory(tmpdir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
|
||||
withGit(
|
||||
getRepositoryDir(originUserName, originRepositoryName),
|
||||
getRepositoryDir(forkedRepository.owner, forkedRepository.name)
|
||||
){ (oldGit, newGit) =>
|
||||
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
||||
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
||||
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
case _ => {
|
||||
JGitUtil.withGit(getRepositoryDir(forkedRepository.owner, forkedRepository.name)){ git =>
|
||||
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
|
||||
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
|
||||
|
||||
(getRepository(originOwner, repository.name, baseUrl),
|
||||
getRepository(forkedOwner, repository.name, baseUrl)) match {
|
||||
case (Some(originRepository), Some(forkedRepository)) => {
|
||||
withGit(
|
||||
getRepositoryDir(originOwner, repository.name),
|
||||
getRepositoryDir(forkedOwner, repository.name)
|
||||
){ case (oldGit, newGit) =>
|
||||
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
|
||||
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
|
||||
|
||||
val forkedId = getForkedCommitId(oldGit, newGit,
|
||||
originOwner, repository.name, originBranch,
|
||||
forkedOwner, repository.name, forkedBranch)
|
||||
|
||||
val oldId = oldGit.getRepository.resolve(forkedId)
|
||||
val newId = newGit.getRepository.resolve(forkedBranch)
|
||||
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originOwner, repository.name, oldId.getName,
|
||||
forkedOwner, repository.name, newId.getName)
|
||||
|
||||
pulls.html.compare(
|
||||
commits,
|
||||
diffs,
|
||||
repository.repository.originUserName.map { userName =>
|
||||
userName :: getForkedRepositories(userName, repository.name)
|
||||
} getOrElse List(repository.owner),
|
||||
originBranch,
|
||||
forkedBranch,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch),
|
||||
repository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
}
|
||||
}
|
||||
case _ => NotFound
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
|
||||
val issueId = createIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginUserName,
|
||||
title = form.title,
|
||||
content = form.content,
|
||||
assignedUserName = None,
|
||||
milestoneId = None,
|
||||
isPullRequest = true)
|
||||
|
||||
createPullRequest(
|
||||
originUserName = repository.owner,
|
||||
originRepositoryName = repository.name,
|
||||
issueId = issueId,
|
||||
originBranch = form.targetBranch,
|
||||
requestUserName = form.requestUserName,
|
||||
requestRepositoryName = repository.name,
|
||||
requestBranch = form.requestBranch,
|
||||
commitIdFrom = form.commitIdFrom,
|
||||
commitIdTo = form.commitIdTo)
|
||||
|
||||
// fetch requested branch
|
||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
|
||||
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
|
||||
.call
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
})
|
||||
|
||||
/**
|
||||
* Handles w Git object simultaneously.
|
||||
*/
|
||||
private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = {
|
||||
val oldGit = Git.open(oldDir)
|
||||
val newGit = Git.open(newDir)
|
||||
try {
|
||||
action(oldGit, newGit)
|
||||
} finally {
|
||||
oldGit.getRepository.close
|
||||
newGit.getRepository.close
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
*
|
||||
* - "owner:branch" to ("owner", "branch")
|
||||
* - "branch" to ("defaultOwner", "branch")
|
||||
*/
|
||||
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
|
||||
if(value.contains(':')){
|
||||
val array = value.split(":")
|
||||
(array(0), array(1))
|
||||
} else {
|
||||
(defaultOwner, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
|
||||
*/
|
||||
private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
|
||||
node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
|
||||
|
||||
/**
|
||||
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
|
||||
*/
|
||||
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
|
||||
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
|
||||
existsCommitId(userName, repositoryName, commit.getName) &&
|
||||
JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
|
||||
}.head.id
|
||||
|
||||
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
|
||||
|
||||
withGit(
|
||||
getRepositoryDir(userName, repositoryName),
|
||||
getRepositoryDir(requestUserName, requestRepositoryName)
|
||||
){ (oldGit, newGit) =>
|
||||
val oldId = oldGit.getRepository.resolve(branch)
|
||||
val newId = newGit.getRepository.resolve(requestCommitId)
|
||||
|
||||
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
|
||||
new CommitInfo(revCommit)
|
||||
}.toList.splitWith{ (commit1, commit2) =>
|
||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
||||
}
|
||||
|
||||
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
|
||||
|
||||
(commits, diffs)
|
||||
}
|
||||
}
|
||||
|
||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = {
|
||||
val owner = repository.owner
|
||||
val repoName = repository.name
|
||||
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val sessionKey = s"${owner}/${repoName}/pulls"
|
||||
|
||||
// retrieve search condition
|
||||
val condition = if(request.getQueryString == null){
|
||||
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
|
||||
} else IssueSearchCondition(request)
|
||||
|
||||
session.put(sessionKey, condition)
|
||||
|
||||
pulls.html.list(
|
||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
|
||||
userName,
|
||||
page,
|
||||
countIssue(condition.copy(state = "open"), filterUser, true, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
|
||||
countIssue(condition, Map.empty, true, owner -> repoName),
|
||||
condition,
|
||||
repository,
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import util.Directory._
|
||||
import util.{UsersAuthenticator, OwnerAuthenticator}
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.FlashMapSupport
|
||||
|
||||
class SettingsController extends SettingsControllerBase
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
|
||||
|
||||
trait SettingsControllerBase extends ControllerBase {
|
||||
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator =>
|
||||
|
||||
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
||||
@@ -30,53 +31,61 @@ trait SettingsControllerBase extends ControllerBase {
|
||||
* Redirect to the Options page.
|
||||
*/
|
||||
get("/:owner/:repository/settings")(ownerOnly { repository =>
|
||||
redirect("/%s/%s/settings/options".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the Options page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/options")(ownerOnly {
|
||||
settings.html.options(_)
|
||||
settings.html.options(_, flash.get("info"))
|
||||
})
|
||||
|
||||
/**
|
||||
* Save the repository options.
|
||||
*/
|
||||
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
||||
saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate)
|
||||
redirect("/%s/%s/settings/options".format(repository.owner, repository.name))
|
||||
saveRepositoryOptions(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.description,
|
||||
form.defaultBranch,
|
||||
repository.repository.parentUserName.map { _ =>
|
||||
repository.repository.isPrivate
|
||||
} getOrElse form.isPrivate
|
||||
)
|
||||
flash += "info" -> "Repository settings has been updated."
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the Collaborators page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
|
||||
settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* JSON API for collaborator completion.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
|
||||
settings.html.collaborators(
|
||||
getCollaborators(repository.owner, repository.name),
|
||||
getAccountByUserName(repository.owner).get.isGroupAccount,
|
||||
repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* Add the collaborator.
|
||||
*/
|
||||
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
|
||||
addCollaborator(repository.owner, repository.name, form.userName)
|
||||
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
|
||||
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||
addCollaborator(repository.owner, repository.name, form.userName)
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||
})
|
||||
|
||||
/**
|
||||
* Add the collaborator.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
|
||||
removeCollaborator(repository.owner, repository.name, params("name"))
|
||||
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
|
||||
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||
removeCollaborator(repository.owner, repository.name, params("name"))
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -96,25 +105,20 @@ trait SettingsControllerBase extends ControllerBase {
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
|
||||
|
||||
redirect("/%s".format(repository.owner))
|
||||
redirect(s"/${repository.owner}")
|
||||
})
|
||||
|
||||
/**
|
||||
* Provides Constraint to validate the collaborator name.
|
||||
*/
|
||||
private def collaborator: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] = {
|
||||
override def validate(name: String, value: String): Option[String] = {
|
||||
val paths = request.getRequestURI.split("/")
|
||||
getAccountByUserName(value) match {
|
||||
case None => Some("User does not exist.")
|
||||
case Some(x) if(x.userName == context.loginAccount.get.userName) => Some("User can access this repository already.")
|
||||
case Some(x) => {
|
||||
val paths = request.getRequestURI.split("/")
|
||||
if(getCollaborators(paths(1), paths(2)).contains(x.userName)){
|
||||
Some("User can access this repository already.")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName))
|
||||
=> Some("User can access this repository already.")
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package app
|
||||
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil}
|
||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
|
||||
import service._
|
||||
import org.scalatra._
|
||||
import java.io.File
|
||||
@@ -27,8 +27,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
contentType = "text/html"
|
||||
view.helpers.markdown(params("content"), repository,
|
||||
params("enableWikiLink").toBoolean,
|
||||
params("enableCommitLink").toBoolean,
|
||||
params("enableIssueLink").toBoolean)
|
||||
params("enableRefsLink").toBoolean)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -38,61 +37,43 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
fileList(_)
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the file list of the repository root and the specified branch.
|
||||
*/
|
||||
get("/:owner/:repository/tree/:id")(referrersOnly {
|
||||
fileList(_, params("id"))
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the file list of the specified path and branch.
|
||||
*/
|
||||
get("/:owner/:repository/tree/:id/*")(referrersOnly {
|
||||
fileList(_, params("id"), multiParams("splat").head)
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the commit list of the specified branch.
|
||||
*/
|
||||
get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
|
||||
val branchName = params("branch")
|
||||
val page = params.getOrElse("page", "1").toInt
|
||||
|
||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30)
|
||||
|
||||
repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
|
||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
||||
}, page, hasNext)
|
||||
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
|
||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||
if(path.isEmpty){
|
||||
fileList(repository, id)
|
||||
} else {
|
||||
fileList(repository, id, path)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the commit list of the specified resource.
|
||||
*/
|
||||
get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository =>
|
||||
val branchName = params("branch")
|
||||
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
|
||||
val page = params.getOrElse("page", "1").toInt
|
||||
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
|
||||
val (branchName, path) = splitPath(repository, multiParams("splat").head)
|
||||
val page = params.getOrElse("page", "1").toInt
|
||||
|
||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30, path)
|
||||
|
||||
repo.html.commits(path.split("/").toList, branchName, repository,
|
||||
logs.splitWith{ (commit1, commit2) =>
|
||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
||||
}, page, hasNext)
|
||||
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
||||
case Right((logs, hasNext)) =>
|
||||
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
|
||||
logs.splitWith{ (commit1, commit2) =>
|
||||
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
|
||||
}, page, hasNext)
|
||||
case Left(_) => NotFound
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the file content of the specified branch or commit.
|
||||
*/
|
||||
get("/:owner/:repository/blob/:id/*")(referrersOnly { repository =>
|
||||
val id = params("id") // branch name or commit id
|
||||
val raw = params.get("raw").getOrElse("false").toBoolean
|
||||
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
|
||||
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||
val raw = params.get("raw").getOrElse("false").toBoolean
|
||||
|
||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||
@@ -125,7 +106,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
val content = if(viewer == "other"){
|
||||
if(bytes.isDefined && FileUtil.isText(bytes.get)){
|
||||
// text
|
||||
JGitUtil.ContentInfo("text", bytes.map(new String(_, "UTF-8")))
|
||||
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
|
||||
} else {
|
||||
// binary
|
||||
JGitUtil.ContentInfo("binary", None)
|
||||
@@ -149,19 +130,36 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||
|
||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||
repository, JGitUtil.getDiffs(git, id))
|
||||
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
|
||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||
repository, diffs, oldCommitId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays branches.
|
||||
*/
|
||||
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
// retrieve latest update date of each branch
|
||||
val branchInfo = repository.branchList.map { branchName =>
|
||||
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
|
||||
(branchName, revCommit.getCommitterIdent.getWhen)
|
||||
}
|
||||
repo.html.branches(branchInfo, repository)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays tags.
|
||||
*/
|
||||
get("/:owner/:repository/tags")(referrersOnly {
|
||||
repo.html.tags(_)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Download repository contents as an archive.
|
||||
*/
|
||||
@@ -200,7 +198,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
BadRequest
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/network/members")(referrersOnly { repository =>
|
||||
repo.html.forked(
|
||||
getRepository(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name),
|
||||
baseUrl),
|
||||
getForkedRepositories(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||
repository)
|
||||
})
|
||||
|
||||
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
|
||||
val id = repository.branchList.collectFirst {
|
||||
case branch if(path == branch || path.startsWith(branch + "/")) => branch
|
||||
} orElse repository.tags.collectFirst {
|
||||
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
|
||||
} orElse Some(path.split("/")(0)) get
|
||||
|
||||
(id, path.substring(id.length).replaceFirst("^/", ""))
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides HTML of the file list.
|
||||
*
|
||||
@@ -210,40 +230,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
* @return HTML of the file list
|
||||
*/
|
||||
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
|
||||
getRepository(repository.owner, repository.name, baseUrl).map { repositoryInfo =>
|
||||
val revision = if(revstr.isEmpty){
|
||||
repositoryInfo.repository.defaultBranch
|
||||
} else {
|
||||
revstr
|
||||
if(repository.commitCount == 0){
|
||||
repo.html.guide(repository)
|
||||
} else {
|
||||
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
|
||||
// get specified commit
|
||||
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
|
||||
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
// process README.md
|
||||
val readme = files.find(_.name == "README.md").map { file =>
|
||||
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
}
|
||||
|
||||
repo.html.files(revision, repository,
|
||||
if(path == ".") Nil else path.split("/").toList, // current path
|
||||
new JGitUtil.CommitInfo(revCommit), // latest commit
|
||||
files, readme)
|
||||
} getOrElse NotFound
|
||||
}
|
||||
|
||||
JGitUtil.withGit(getRepositoryDir(repositoryInfo.owner, repositoryInfo.name)){ git =>
|
||||
// get latest commit
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
|
||||
// process README.md
|
||||
val readme = files.find(_.name == "README.md").map { file =>
|
||||
new String(JGitUtil.getContent(Git.open(getRepositoryDir(repositoryInfo.owner, repositoryInfo.name)), file.id, true).get, "UTF-8")
|
||||
}
|
||||
|
||||
repo.html.files(
|
||||
// current branch
|
||||
revision,
|
||||
// repository
|
||||
repositoryInfo,
|
||||
// current path
|
||||
if(path == ".") Nil else path.split("/").toList,
|
||||
// latest commit
|
||||
new JGitUtil.CommitInfo(revCommit),
|
||||
// file list
|
||||
files,
|
||||
// readme
|
||||
readme
|
||||
)
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
51
src/main/scala/app/SearchController.scala
Normal file
51
src/main/scala/app/SearchController.scala
Normal file
@@ -0,0 +1,51 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SearchController extends SearchControllerBase
|
||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
||||
with RepositorySearchService with IssuesService
|
||||
with ReferrerAuthenticator
|
||||
|
||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||
with SystemSettingsService with ActivityService with RepositorySearchService
|
||||
with ReferrerAuthenticator =>
|
||||
|
||||
val searchForm = mapping(
|
||||
"query" -> trim(text(required)),
|
||||
"owner" -> trim(text(required)),
|
||||
"repository" -> trim(text(required))
|
||||
)(SearchForm.apply)
|
||||
|
||||
case class SearchForm(query: String, owner: String, repository: String)
|
||||
|
||||
post("/search", searchForm){ form =>
|
||||
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||
}
|
||||
|
||||
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||
val query = params("q").trim
|
||||
val target = params.getOrElse("type", "code")
|
||||
val page = try {
|
||||
val i = params.getOrElse("page", "1").toInt
|
||||
if(i <= 0) 1 else i
|
||||
} catch {
|
||||
case e: NumberFormatException => 1
|
||||
}
|
||||
|
||||
target.toLowerCase match {
|
||||
case "issue" => search.html.issues(
|
||||
searchIssues(repository.owner, repository.name, query),
|
||||
countFiles(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
|
||||
case _ => search.html.code(
|
||||
searchFiles(repository.owner, repository.name, query),
|
||||
countIssues(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.StringUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
|
||||
@@ -16,27 +15,18 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
|
||||
)(SignInForm.apply)
|
||||
|
||||
get("/signin"){
|
||||
val queryString = request.getQueryString
|
||||
if(queryString != null && queryString.startsWith("/")){
|
||||
session.setAttribute("REDIRECT", queryString)
|
||||
val redirect = params.get("redirect")
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
session.setAttribute("REDIRECT", redirect.get)
|
||||
}
|
||||
html.signin(loadSystemSettings())
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
val account = getAccountByUserName(form.userName)
|
||||
if(account.isEmpty || account.get.password != encrypt(form.password)){
|
||||
redirect("/signin")
|
||||
} else {
|
||||
session.setAttribute("LOGIN_ACCOUNT", account.get)
|
||||
updateLastLoginDate(account.get.userName)
|
||||
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/%s".format(account.get.userName))
|
||||
}
|
||||
val settings = loadSystemSettings()
|
||||
authenticate(loadSystemSettings(), form.userName, form.password) match {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +35,19 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
/**
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
private def signin(account: model.Account) = {
|
||||
session.setAttribute("LOGIN_ACCOUNT", account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,26 +4,45 @@ import service.{AccountService, SystemSettingsService}
|
||||
import SystemSettingsService._
|
||||
import util.AdminAuthenticator
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.scalatra.FlashMapSupport
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with SystemSettingsService with AccountService with AdminAuthenticator
|
||||
|
||||
trait SystemSettingsControllerBase extends ControllerBase {
|
||||
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
|
||||
self: SystemSettingsService with AccountService with AdminAuthenticator =>
|
||||
|
||||
private case class SystemSettingsForm(allowAccountRegistration: Boolean)
|
||||
|
||||
private val form = mapping(
|
||||
"allowAccountRegistration" -> trim(label("Account registration", boolean()))
|
||||
)(SystemSettingsForm.apply)
|
||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"smtp" -> optionalIfNotChecked("notification", mapping(
|
||||
"host" -> trim(label("SMTP Host", text(required))),
|
||||
"port" -> trim(label("SMTP Port", optional(number()))),
|
||||
"user" -> trim(label("SMTP User", optional(text()))),
|
||||
"password" -> trim(label("SMTP Password", optional(text()))),
|
||||
"ssl" -> trim(label("Enable SSL", optional(boolean())))
|
||||
)(Smtp.apply)),
|
||||
"ldapAuthentication" -> trim(label("LDAP", boolean())),
|
||||
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
|
||||
"host" -> trim(label("LDAP host", text(required))),
|
||||
"port" -> trim(label("LDAP port", optional(number()))),
|
||||
"bindDN" -> trim(label("Bind DN", optional(text()))),
|
||||
"bindPassword" -> trim(label("Bind Password", optional(text()))),
|
||||
"baseDN" -> trim(label("Base DN", text(required))),
|
||||
"userNameAttribute" -> trim(label("User name attribute", text(required))),
|
||||
"mailAttribute" -> trim(label("Mail address attribute", text(required)))
|
||||
)(Ldap.apply))
|
||||
)(SystemSettings.apply)
|
||||
|
||||
|
||||
get("/admin/system")(adminOnly {
|
||||
admin.html.system(loadSystemSettings())
|
||||
admin.html.system(loadSystemSettings(), flash.get("info"))
|
||||
})
|
||||
|
||||
post("/admin/system", form)(adminOnly { form =>
|
||||
saveSystemSettings(SystemSettings(form.allowAccountRegistration))
|
||||
saveSystemSettings(form)
|
||||
flash += "info" -> "System settings has been updated."
|
||||
redirect("/admin/system")
|
||||
})
|
||||
|
||||
|
||||
@@ -5,72 +5,135 @@ import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
|
||||
class UserManagementController extends UserManagementControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
|
||||
trait UserManagementControllerBase extends ControllerBase { self: AccountService with AdminAuthenticator =>
|
||||
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||
|
||||
case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
|
||||
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean, url: Option[String])
|
||||
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String])
|
||||
|
||||
val newForm = mapping(
|
||||
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String])
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String], clearImage: Boolean)
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
||||
)(UserNewForm.apply)
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text())))
|
||||
)(NewUserForm.apply)
|
||||
|
||||
val editForm = mapping(
|
||||
val editUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
||||
)(UserEditForm.apply)
|
||||
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
)(EditUserForm.apply)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" , optional(text())))
|
||||
)(NewGroupForm.apply)
|
||||
|
||||
val editGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
admin.users.html.list(getAllUsers())
|
||||
val users = getAllUsers()
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName)
|
||||
}.toMap
|
||||
admin.users.html.list(users, members)
|
||||
})
|
||||
|
||||
get("/admin/users/_new")(adminOnly {
|
||||
admin.users.html.edit(None)
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
admin.users.html.user(None)
|
||||
})
|
||||
|
||||
post("/admin/users/_new", newForm)(adminOnly { form =>
|
||||
createAccount(form.userName, encrypt(form.password), form.mailAddress, form.isAdmin, form.url)
|
||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:userName/_edit")(adminOnly {
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
admin.users.html.edit(getAccountByUserName(userName))
|
||||
admin.users.html.user(getAccountByUserName(userName))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { account =>
|
||||
updateAccount(getAccountByUserName(userName).get.copy(
|
||||
password = form.password.map(encrypt).getOrElse(account.password),
|
||||
password = form.password.map(sha1).getOrElse(account.password),
|
||||
mailAddress = form.mailAddress,
|
||||
isAdmin = form.isAdmin,
|
||||
url = form.url))
|
||||
|
||||
updateImage(userName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
// TODO Merge with AccountController?
|
||||
private def uniqueUserName: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getAccountByUserName(value).map { _ => "User already exists." }
|
||||
}
|
||||
get("/admin/users/_newgroup")(adminOnly {
|
||||
admin.users.html.group(None, Nil)
|
||||
})
|
||||
|
||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||
createGroup(form.groupName, form.url)
|
||||
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
|
||||
updateImage(form.groupName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
val groupName = params("groupName")
|
||||
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
val groupName = params("groupName")
|
||||
getAccountByUserName(groupName).map { account =>
|
||||
updateGroup(groupName, form.url)
|
||||
|
||||
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
|
||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/admin/users/_usercheck")(adminOnly {
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
})
|
||||
|
||||
// TODO Merge with AccountController?
|
||||
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getAccountByMailAddress(value)
|
||||
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
|
||||
.map { _ => "Mail address is already registered." }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +1,29 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil}
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
|
||||
import util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class WikiController extends WikiControllerBase
|
||||
with WikiService with RepositoryService with AccountService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||
with WikiService with RepositoryService with AccountService with ActivityService
|
||||
with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||
|
||||
trait WikiControllerBase extends ControllerBase {
|
||||
self: WikiService with RepositoryService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||
self: WikiService with RepositoryService with ActivityService
|
||||
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||
|
||||
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier, unique))),
|
||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
|
||||
"content" -> trim(label("Content" , text(required))),
|
||||
"message" -> trim(label("Message" , optional(text()))),
|
||||
"currentPageName" -> trim(label("Current page name" , text()))
|
||||
)(WikiPageEditForm.apply)
|
||||
|
||||
val editForm = mapping(
|
||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier))),
|
||||
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
|
||||
"content" -> trim(label("Content" , text(required))),
|
||||
"message" -> trim(label("Message" , optional(text()))),
|
||||
"currentPageName" -> trim(label("Current page name" , text(required)))
|
||||
@@ -30,31 +32,34 @@ trait WikiControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/wiki")(referrersOnly { repository =>
|
||||
getWikiPage(repository.owner, repository.name, "Home").map { page =>
|
||||
wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect("/%s/%s/wiki/Home/_edit".format(repository.owner, repository.name))
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/:page")(referrersOnly { repository =>
|
||||
val pageName = params("page")
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
|
||||
getWikiPage(repository.owner, repository.name, pageName).map { page =>
|
||||
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
} getOrElse redirect("/%s/%s/wiki/%s/_edit".format(repository.owner, repository.name, pageName)) // TODO URLEncode
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
|
||||
val pageName = params("page")
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
|
||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
wiki.html.history(Some(pageName), JGitUtil.getCommitLog(git, "master", path = pageName + ".md")._1, repository)
|
||||
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
|
||||
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
|
||||
case Left(_) => NotFound
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
|
||||
val pageName = params("page")
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
val commitId = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), repository)
|
||||
wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -62,21 +67,25 @@ trait WikiControllerBase extends ControllerBase {
|
||||
val commitId = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), repository)
|
||||
wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
|
||||
val pageName = params("page")
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
|
||||
val loginAccount = context.loginAccount.get
|
||||
|
||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||
form.content, context.loginAccount.get, form.message.getOrElse(""))
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
form.content, loginAccount, form.message.getOrElse("")).map { commitId =>
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
||||
}
|
||||
|
||||
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
|
||||
@@ -84,20 +93,25 @@ trait WikiControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||
val loginAccount = context.loginAccount.get
|
||||
|
||||
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
|
||||
form.content, context.loginAccount.get, form.message.getOrElse(""))
|
||||
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
|
||||
|
||||
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
|
||||
val pageName = params("page")
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
val account = context.loginAccount.get
|
||||
|
||||
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, "Delete %s".format(pageName))
|
||||
deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}")
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
|
||||
redirect("/%s/%s/wiki".format(repository.owner, repository.name))
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
|
||||
@@ -107,7 +121,10 @@ trait WikiControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
||||
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
|
||||
wiki.html.history(None, JGitUtil.getCommitLog(git, "master")._1, repository)
|
||||
JGitUtil.getCommitLog(git, "master") match {
|
||||
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
|
||||
case Left(_) => NotFound
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -119,8 +136,20 @@ trait WikiControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
private def unique: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
|
||||
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
||||
}
|
||||
|
||||
private def pagename: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
if(value.exists("\\/:*?\"<>|".contains(_))){
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
Some(s"${name} starts with invalid character.")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object Accounts extends Table[Account]("ACCOUNT") with Functions {
|
||||
object Accounts extends Table[Account]("ACCOUNT") {
|
||||
def userName = column[String]("USER_NAME", O PrimaryKey)
|
||||
def mailAddress = column[String]("MAIL_ADDRESS")
|
||||
def password = column[String]("PASSWORD")
|
||||
@@ -11,7 +11,9 @@ object Accounts extends Table[Account]("ACCOUNT") with Functions {
|
||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
||||
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? <> (Account, Account.unapply _)
|
||||
def image = column[String]("IMAGE")
|
||||
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
|
||||
}
|
||||
|
||||
case class Account(
|
||||
@@ -22,5 +24,7 @@ case class Account(
|
||||
url: Option[String],
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date,
|
||||
lastLoginDate: Option[java.util.Date]
|
||||
lastLoginDate: Option[java.util.Date],
|
||||
image: Option[String],
|
||||
isGroupAccount: Boolean
|
||||
)
|
||||
|
||||
31
src/main/scala/model/Activity.scala
Normal file
31
src/main/scala/model/Activity.scala
Normal file
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate {
|
||||
def activityId = column[Int]("ACTIVITY_ID", O AutoInc)
|
||||
def activityUserName = column[String]("ACTIVITY_USER_NAME")
|
||||
def activityType = column[String]("ACTIVITY_TYPE")
|
||||
def message = column[String]("MESSAGE")
|
||||
def additionalInfo = column[String]("ADDITIONAL_INFO")
|
||||
def activityDate = column[java.util.Date]("ACTIVITY_DATE")
|
||||
def * = activityId ~ userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate <> (Activity, Activity.unapply _)
|
||||
def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId
|
||||
}
|
||||
|
||||
object CommitLog extends Table[(String, String, String)]("COMMIT_LOG") with BasicTemplate {
|
||||
def commitId = column[String]("COMMIT_ID")
|
||||
def * = userName ~ repositoryName ~ commitId
|
||||
def byPrimaryKey(userName: String, repositoryName: String, commitId: String) = byRepository(userName, repositoryName) && (this.commitId is commitId.bind)
|
||||
}
|
||||
|
||||
case class Activity(
|
||||
activityId: Int,
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
activityType: String,
|
||||
message: String,
|
||||
additionalInfo: Option[String],
|
||||
activityDate: java.util.Date
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
package model
|
||||
|
||||
import scala.slick.lifted.MappedTypeMapper
|
||||
|
||||
protected[model] trait Functions {
|
||||
// java.util.Date TypeMapper
|
||||
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
|
||||
d => new java.sql.Timestamp(d.getTime),
|
||||
t => new java.util.Date(t.getTime)
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns system date.
|
||||
*/
|
||||
def currentDate = new java.util.Date()
|
||||
|
||||
}
|
||||
14
src/main/scala/model/GroupMembers.scala
Normal file
14
src/main/scala/model/GroupMembers.scala
Normal file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
|
||||
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
|
||||
def userName = column[String]("USER_NAME", O PrimaryKey)
|
||||
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
|
||||
}
|
||||
|
||||
case class GroupMember(
|
||||
groupName: String,
|
||||
userName: String
|
||||
)
|
||||
@@ -7,7 +7,12 @@ object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTempla
|
||||
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
|
||||
}
|
||||
|
||||
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate with Functions {
|
||||
object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
|
||||
def commentCount = column[Int]("COMMENT_COUNT")
|
||||
def * = userName ~ repositoryName ~ issueId ~ commentCount
|
||||
}
|
||||
|
||||
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
|
||||
def openedUserName = column[String]("OPENED_USER_NAME")
|
||||
def assignedUserName = column[String]("ASSIGNED_USER_NAME")
|
||||
def title = column[String]("TITLE")
|
||||
@@ -15,7 +20,8 @@ object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTem
|
||||
def closed = column[Boolean]("CLOSED")
|
||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate <> (Issue, Issue.unapply _)
|
||||
def pullRequest = column[Boolean]("PULL_REQUEST")
|
||||
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
|
||||
}
|
||||
@@ -31,4 +37,5 @@ case class Issue(
|
||||
content: Option[String],
|
||||
closed: Boolean,
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date)
|
||||
updatedDate: java.util.Date,
|
||||
isPullRequest: Boolean)
|
||||
@@ -2,16 +2,16 @@ package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate with Functions {
|
||||
object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate {
|
||||
def commentId = column[Int]("COMMENT_ID", O AutoInc)
|
||||
def action = column[String]("ACTION")
|
||||
def commentedUserName = column[String]("COMMENTED_USER_NAME")
|
||||
def content = column[String]("CONTENT")
|
||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
|
||||
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
|
||||
|
||||
def autoInc = userName ~ repositoryName ~ issueId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
|
||||
def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
|
||||
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ case class IssueComment(
|
||||
repositoryName: String,
|
||||
issueId: Int,
|
||||
commentId: Int,
|
||||
action: Option[String],
|
||||
action: String,
|
||||
commentedUserName: String,
|
||||
content: String,
|
||||
registeredDate: java.util.Date,
|
||||
|
||||
@@ -2,7 +2,7 @@ package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate with Functions {
|
||||
object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate {
|
||||
def title = column[String]("TITLE")
|
||||
def description = column[String]("DESCRIPTION")
|
||||
def dueDate = column[java.util.Date]("DUE_DATE")
|
||||
|
||||
28
src/main/scala/model/PullRequest.scala
Normal file
28
src/main/scala/model/PullRequest.scala
Normal file
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate {
|
||||
def branch = column[String]("BRANCH")
|
||||
def requestUserName = column[String]("REQUEST_USER_NAME")
|
||||
def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
|
||||
def requestBranch = column[String]("REQUEST_BRANCH")
|
||||
def commitIdFrom = column[String]("COMMIT_ID_FROM")
|
||||
def commitIdTo = column[String]("COMMIT_ID_TO")
|
||||
def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _)
|
||||
|
||||
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
|
||||
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
|
||||
}
|
||||
|
||||
case class PullRequest(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
issueId: Int,
|
||||
branch: String,
|
||||
requestUserName: String,
|
||||
requestRepositoryName: String,
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String
|
||||
)
|
||||
@@ -2,14 +2,18 @@ package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate with Functions {
|
||||
object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate {
|
||||
def isPrivate = column[Boolean]("PRIVATE")
|
||||
def description = column[String]("DESCRIPTION")
|
||||
def defaultBranch = column[String]("DEFAULT_BRANCH")
|
||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
|
||||
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate <> (Repository, Repository.unapply _)
|
||||
def originUserName = column[String]("ORIGIN_USER_NAME")
|
||||
def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
|
||||
def parentUserName = column[String]("PARENT_USER_NAME")
|
||||
def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
|
||||
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
|
||||
}
|
||||
@@ -22,5 +26,9 @@ case class Repository(
|
||||
defaultBranch: String,
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date,
|
||||
lastActivityDate: java.util.Date
|
||||
lastActivityDate: java.util.Date,
|
||||
originUserName: Option[String],
|
||||
originRepositoryName: Option[String],
|
||||
parentUserName: Option[String],
|
||||
parentRepositoryName: Option[String]
|
||||
)
|
||||
|
||||
20
src/main/scala/model/package.scala
Normal file
20
src/main/scala/model/package.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package object model {
|
||||
import scala.slick.driver.BasicDriver.Implicit._
|
||||
import scala.slick.lifted.{Column, MappedTypeMapper}
|
||||
|
||||
// java.util.Date TypeMapper
|
||||
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
|
||||
d => new java.sql.Timestamp(d.getTime),
|
||||
t => new java.util.Date(t.getTime)
|
||||
)
|
||||
|
||||
implicit class RichColumn(c1: Column[Boolean]){
|
||||
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns system date.
|
||||
*/
|
||||
def currentDate = new java.util.Date()
|
||||
|
||||
}
|
||||
@@ -1,12 +1,50 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import Accounts._
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
import service.SystemSettingsService.SystemSettings
|
||||
import util.StringUtil._
|
||||
import model.GroupMember
|
||||
import scala.Some
|
||||
import model.Account
|
||||
import util.LDAPUtil
|
||||
|
||||
trait AccountService {
|
||||
|
||||
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
|
||||
if(settings.ldapAuthentication){
|
||||
ldapAuthentication(settings, userName, password)
|
||||
} else {
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate by internal database.
|
||||
*/
|
||||
private def defaultAuthentication(userName: String, password: String) = {
|
||||
getAccountByUserName(userName).collect {
|
||||
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
|
||||
} getOrElse None
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate by LDAP.
|
||||
*/
|
||||
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
|
||||
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
|
||||
case Right(mailAddress) => {
|
||||
// Create or update account by LDAP information
|
||||
getAccountByUserName(userName) match {
|
||||
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
|
||||
case None => createAccount(userName, "", mailAddress, false, None)
|
||||
}
|
||||
getAccountByUserName(userName)
|
||||
}
|
||||
case Left(errorMessage) => defaultAuthentication(userName, password)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByUserName(userName: String): Option[Account] =
|
||||
Query(Accounts) filter(_.userName is userName.bind) firstOption
|
||||
|
||||
@@ -24,7 +62,9 @@ trait AccountService {
|
||||
url = url,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None)
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = false)
|
||||
|
||||
def updateAccount(account: Account): Unit =
|
||||
Accounts
|
||||
@@ -38,8 +78,48 @@ trait AccountService {
|
||||
account.registeredDate,
|
||||
currentDate,
|
||||
account.lastLoginDate)
|
||||
|
||||
|
||||
def updateAvatarImage(userName: String, image: Option[String]): Unit =
|
||||
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
|
||||
|
||||
def updateLastLoginDate(userName: String): Unit =
|
||||
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
|
||||
|
||||
|
||||
def createGroup(groupName: String, url: Option[String]): Unit =
|
||||
Accounts insert Account(
|
||||
userName = groupName,
|
||||
password = "",
|
||||
mailAddress = groupName + "@devnull",
|
||||
isAdmin = false,
|
||||
url = url,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = true)
|
||||
|
||||
def updateGroup(groupName: String, url: Option[String]): Unit =
|
||||
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
|
||||
|
||||
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
|
||||
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
|
||||
members.foreach { userName =>
|
||||
GroupMembers insert GroupMember (groupName, userName)
|
||||
}
|
||||
}
|
||||
|
||||
def getGroupMembers(groupName: String): List[String] =
|
||||
Query(GroupMembers)
|
||||
.filter(_.groupName is groupName.bind)
|
||||
.sortBy(_.userName)
|
||||
.map(_.userName)
|
||||
.list
|
||||
|
||||
def getGroupsByUserName(userName: String): List[String] =
|
||||
Query(GroupMembers)
|
||||
.filter(_.userName is userName.bind)
|
||||
.sortBy(_.groupName)
|
||||
.map(_.groupName)
|
||||
.list
|
||||
|
||||
}
|
||||
|
||||
150
src/main/scala/service/ActivityService.scala
Normal file
150
src/main/scala/service/ActivityService.scala
Normal file
@@ -0,0 +1,150 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
|
||||
trait ActivityService {
|
||||
|
||||
def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] =
|
||||
Activities
|
||||
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||
.filter { case (t1, t2) =>
|
||||
if(isPublic){
|
||||
(t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind)
|
||||
} else {
|
||||
(t1.activityUserName is activityUserName.bind)
|
||||
}
|
||||
}
|
||||
.sortBy { case (t1, t2) => t1.activityId desc }
|
||||
.map { case (t1, t2) => t1 }
|
||||
.take(30)
|
||||
.list
|
||||
|
||||
def getRecentActivities(): List[Activity] =
|
||||
Activities
|
||||
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||
.filter { case (t1, t2) => t2.isPrivate is false.bind }
|
||||
.sortBy { case (t1, t2) => t1.activityId desc }
|
||||
.map { case (t1, t2) => t1 }
|
||||
.take(30)
|
||||
.list
|
||||
|
||||
|
||||
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"create_repository",
|
||||
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate)
|
||||
|
||||
def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"open_issue",
|
||||
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
currentDate)
|
||||
|
||||
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"close_issue",
|
||||
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
currentDate)
|
||||
|
||||
def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"close_issue",
|
||||
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
currentDate)
|
||||
|
||||
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"reopen_issue",
|
||||
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
currentDate)
|
||||
|
||||
def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"comment_issue",
|
||||
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(cut(comment, 200)),
|
||||
currentDate)
|
||||
|
||||
def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"comment_issue",
|
||||
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(cut(comment, 200)),
|
||||
currentDate)
|
||||
|
||||
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"create_wiki",
|
||||
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
|
||||
Some(pageName),
|
||||
currentDate)
|
||||
|
||||
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"edit_wiki",
|
||||
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
|
||||
Some(pageName + ":" + commitId),
|
||||
currentDate)
|
||||
|
||||
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
|
||||
branchName: String, commits: List[util.JGitUtil.CommitInfo]) =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"push",
|
||||
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
||||
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
|
||||
currentDate)
|
||||
|
||||
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
|
||||
tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"create_tag",
|
||||
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate)
|
||||
|
||||
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"create_branch",
|
||||
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate)
|
||||
|
||||
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"fork",
|
||||
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate)
|
||||
|
||||
def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"open_pullreq",
|
||||
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
currentDate)
|
||||
|
||||
def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"merge_pullreq",
|
||||
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(message),
|
||||
currentDate)
|
||||
|
||||
def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
|
||||
CommitLog insert (userName, repositoryName, commitId)
|
||||
}
|
||||
|
||||
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
|
||||
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined
|
||||
|
||||
private def cut(value: String, length: Int): String =
|
||||
if(value.length > length) value.substring(0, length) + "..." else value
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
|
||||
import Q.interpolation
|
||||
|
||||
import model._
|
||||
import Issues._
|
||||
import util.Implicits._
|
||||
import util.StringUtil._
|
||||
|
||||
trait IssuesService {
|
||||
import IssuesService._
|
||||
@@ -36,21 +36,24 @@ trait IssuesService {
|
||||
.map ( _._2 )
|
||||
.list
|
||||
|
||||
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
|
||||
Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
|
||||
|
||||
/**
|
||||
* Returns the count of the search result against issues.
|
||||
*
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param condition the search condition
|
||||
* @param filter the filter type ("all", "assigned" or "created_by")
|
||||
* @param userName the filter user name required for "assigned" and "created_by"
|
||||
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
|
||||
* @param repos Tuple of the repository owner and the repository name
|
||||
* @return the count of the search result
|
||||
*/
|
||||
def countIssue(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]): Int = {
|
||||
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||
repos: (String, String)*): Int = {
|
||||
// TODO It must be _.length instead of map (_.issueId) list).length.
|
||||
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
|
||||
// https://github.com/slick/slick/issues/170
|
||||
(searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length
|
||||
(searchIssueQuery(repos, condition, filterUser, onlyPullRequest) map (_.issueId) list).length
|
||||
}
|
||||
/**
|
||||
* Returns the Map which contains issue count for each labels.
|
||||
@@ -58,14 +61,13 @@ trait IssuesService {
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param condition the search condition
|
||||
* @param filter the filter type ("all", "assigned" or "created_by")
|
||||
* @param userName the filter user name required for "assigned" and "created_by"
|
||||
* @return the Map which contains issue count for each labels (key is label name, value is issue count),
|
||||
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
|
||||
*/
|
||||
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
|
||||
filter: String, userName: Option[String]): Map[String, Int] = {
|
||||
filterUser: Map[String, String]): Map[String, Int] = {
|
||||
|
||||
searchIssueQuery(owner, repository, condition.copy(labels = Set.empty), filter, userName)
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
|
||||
.innerJoin(IssueLabels).on { (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
}
|
||||
@@ -80,73 +82,100 @@ trait IssuesService {
|
||||
}
|
||||
.toMap
|
||||
}
|
||||
/**
|
||||
* Returns list which contains issue count for each repository.
|
||||
* If the issue does not exist, its repository is not included in the result.
|
||||
*
|
||||
* @param condition the search condition
|
||||
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
||||
* @param repos Tuple of the repository owner and the repository name
|
||||
* @return list which contains issue count for each repository
|
||||
*/
|
||||
def countIssueGroupByRepository(
|
||||
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||
repos: (String, String)*): List[(String, String, Int)] = {
|
||||
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
|
||||
.groupBy { t =>
|
||||
t.userName ~ t.repositoryName
|
||||
}
|
||||
.map { case (repo, t) =>
|
||||
repo ~ t.length
|
||||
}
|
||||
.sortBy(_._3 desc)
|
||||
.list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search result against issues.
|
||||
*
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param condition the search condition
|
||||
* @param filter the filter type ("all", "assigned" or "created_by")
|
||||
* @param userName the filter user name required for "assigned" and "created_by"
|
||||
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
|
||||
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
||||
* @param offset the offset for pagination
|
||||
* @param limit the limit for pagination
|
||||
* @param repos Tuple of the repository owner and the repository name
|
||||
* @return the search result (list of tuples which contain issue, labels and comment count)
|
||||
*/
|
||||
def searchIssue(owner: String, repository: String, condition: IssueSearchCondition,
|
||||
filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = {
|
||||
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||
offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
|
||||
|
||||
// get issues and comment count
|
||||
val issues = searchIssueQuery(owner, repository, condition, filter, userName)
|
||||
.leftJoin(Query(IssueComments)
|
||||
.filter { _.byRepository(owner, repository) }
|
||||
.groupBy { _.issueId }
|
||||
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
|
||||
.sortBy { case (t1, t2) =>
|
||||
(condition.sort match {
|
||||
case "created" => t1.registeredDate
|
||||
case "comments" => t2._2
|
||||
case "updated" => t1.updatedDate
|
||||
}) match {
|
||||
case sort => condition.direction match {
|
||||
case "asc" => sort asc
|
||||
case "desc" => sort desc
|
||||
// get issues and comment count and labels
|
||||
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
|
||||
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
||||
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
|
||||
.map { case (((t1, t2), t3), t4) =>
|
||||
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
||||
}
|
||||
.sortBy(_._4) // labelName
|
||||
.sortBy { case (t1, commentCount, _,_,_) =>
|
||||
(condition.sort match {
|
||||
case "created" => t1.registeredDate
|
||||
case "comments" => commentCount
|
||||
case "updated" => t1.updatedDate
|
||||
}) match {
|
||||
case sort => condition.direction match {
|
||||
case "asc" => sort asc
|
||||
case "desc" => sort desc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.map { case (t1, t2) => (t1, t2._2.ifNull(0)) }
|
||||
.drop(offset).take(limit)
|
||||
.list
|
||||
|
||||
// get labels
|
||||
val labels = Query(IssueLabels)
|
||||
.innerJoin(Labels).on { (t1, t2) =>
|
||||
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
|
||||
}
|
||||
.filter { case (t1, t2) =>
|
||||
(t1.byRepository(owner, repository)) &&
|
||||
(t1.issueId inSetBind (issues.map(_._1.issueId)))
|
||||
}
|
||||
.sortBy { case (t1, t2) => t1.issueId ~ t2.labelName }
|
||||
.map { case (t1, t2) => (t1.issueId, t2) }
|
||||
.list
|
||||
|
||||
issues.map { case (issue, commentCount) =>
|
||||
(issue, labels.collect { case (issueId, labels) if(issueId == issue.issueId) => labels }, commentCount)
|
||||
}
|
||||
.drop(offset).take(limit)
|
||||
.list
|
||||
.splitWith { (c1, c2) =>
|
||||
c1._1.userName == c2._1.userName &&
|
||||
c1._1.repositoryName == c2._1.repositoryName &&
|
||||
c1._1.issueId == c2._1.issueId
|
||||
}
|
||||
.map { issues => issues.head match {
|
||||
case (issue, commentCount, _,_,_) =>
|
||||
(issue,
|
||||
issues.flatMap { t => t._3.map (
|
||||
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
|
||||
)} toList,
|
||||
commentCount)
|
||||
}} toList
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles query for conditional issue searching.
|
||||
*/
|
||||
private def searchIssueQuery(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]) =
|
||||
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
|
||||
filterUser: Map[String, String], onlyPullRequest: Boolean) =
|
||||
Query(Issues) filter { t1 =>
|
||||
(t1.byRepository(owner, repository)) &&
|
||||
condition.repo
|
||||
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
|
||||
.getOrElse (repos)
|
||||
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
||||
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
||||
(t1.closed is (condition.state == "closed").bind) &&
|
||||
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
||||
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
|
||||
(t1.assignedUserName is userName.get.bind, filter == "assigned") &&
|
||||
(t1.openedUserName is userName.get.bind, filter == "created_by") &&
|
||||
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
|
||||
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
|
||||
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
|
||||
(t1.pullRequest is true.bind, onlyPullRequest) &&
|
||||
(IssueLabels filter { t2 =>
|
||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
|
||||
(t2.labelId in
|
||||
@@ -158,7 +187,7 @@ trait IssuesService {
|
||||
}
|
||||
|
||||
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
||||
assignedUserName: Option[String], milestoneId: Option[Int]) =
|
||||
assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
|
||||
// next id number
|
||||
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
|
||||
.firstOption.filter { id =>
|
||||
@@ -173,7 +202,8 @@ trait IssuesService {
|
||||
content,
|
||||
false,
|
||||
currentDate,
|
||||
currentDate)
|
||||
currentDate,
|
||||
isPullRequest)
|
||||
|
||||
// increment issue id
|
||||
IssueId
|
||||
@@ -189,7 +219,7 @@ trait IssuesService {
|
||||
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
|
||||
|
||||
def createComment(owner: String, repository: String, loginUser: String,
|
||||
issueId: Int, content: String, action: Option[String]) =
|
||||
issueId: Int, content: String, action: String) =
|
||||
IssueComments.autoInc insert (
|
||||
owner,
|
||||
repository,
|
||||
@@ -231,10 +261,63 @@ trait IssuesService {
|
||||
}
|
||||
.update (closed, currentDate)
|
||||
|
||||
/**
|
||||
* Search issues by keyword.
|
||||
*
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param query the keywords separated by whitespace.
|
||||
* @return issues with comment count and matched content of issue or comment
|
||||
*/
|
||||
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
|
||||
import scala.slick.driver.H2Driver.likeEncode
|
||||
val keywords = splitWords(query.toLowerCase)
|
||||
|
||||
// Search Issue
|
||||
val issues = Issues
|
||||
.innerJoin(IssueOutline).on { case (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
}
|
||||
.filter { case (t1, t2) =>
|
||||
keywords.map { keyword =>
|
||||
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
|
||||
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
|
||||
} .reduceLeft(_ && _)
|
||||
}
|
||||
.map { case (t1, t2) =>
|
||||
(t1, 0, t1.content.?, t2.commentCount)
|
||||
}
|
||||
|
||||
// Search IssueComment
|
||||
val comments = IssueComments
|
||||
.innerJoin(Issues).on { case (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
}
|
||||
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
|
||||
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
|
||||
}
|
||||
.filter { case ((t1, t2), t3) =>
|
||||
keywords.map { query =>
|
||||
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
|
||||
}.reduceLeft(_ && _)
|
||||
}
|
||||
.map { case ((t1, t2), t3) =>
|
||||
(t2, t1.commentId, t1.content.?, t3.commentCount)
|
||||
}
|
||||
|
||||
issues.union(comments).sortBy { case (issue, commentId, _, _) =>
|
||||
issue.issueId ~ commentId
|
||||
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
|
||||
issue1.issueId == issue2.issueId
|
||||
}.map { _.head match {
|
||||
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
|
||||
}
|
||||
}.toList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object IssuesService {
|
||||
import java.net.URLEncoder
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
val IssueLimit = 30
|
||||
@@ -242,12 +325,11 @@ object IssuesService {
|
||||
case class IssueSearchCondition(
|
||||
labels: Set[String] = Set.empty,
|
||||
milestoneId: Option[Option[Int]] = None,
|
||||
repo: Option[String] = None,
|
||||
state: String = "open",
|
||||
sort: String = "created",
|
||||
direction: String = "desc"){
|
||||
|
||||
import IssueSearchCondition._
|
||||
|
||||
def toURL: String =
|
||||
"?" + List(
|
||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
|
||||
@@ -255,6 +337,7 @@ object IssuesService {
|
||||
case Some(x) => x.toString
|
||||
case None => "none"
|
||||
})},
|
||||
repo.map("for=" + urlEncode(_)),
|
||||
Some("state=" + urlEncode(state)),
|
||||
Some("sort=" + urlEncode(sort)),
|
||||
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
|
||||
@@ -263,8 +346,6 @@ object IssuesService {
|
||||
|
||||
object IssueSearchCondition {
|
||||
|
||||
private def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
|
||||
|
||||
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
|
||||
val value = request.getParameter(name)
|
||||
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
||||
@@ -277,8 +358,17 @@ object IssuesService {
|
||||
case "none" => None
|
||||
case x => Some(x.toInt)
|
||||
}),
|
||||
param(request, "for"),
|
||||
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
||||
|
||||
def page(request: HttpServletRequest) = try {
|
||||
val i = param(request, "page").getOrElse("1").toInt
|
||||
if(i <= 0) 1 else i
|
||||
} catch {
|
||||
case e: NumberFormatException => 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
|
||||
import model._
|
||||
import Milestones._
|
||||
|
||||
trait MilestonesService {
|
||||
|
||||
|
||||
57
src/main/scala/service/PullRequestService.scala
Normal file
57
src/main/scala/service/PullRequestService.scala
Normal file
@@ -0,0 +1,57 @@
|
||||
package service
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
|
||||
import model._
|
||||
|
||||
trait PullRequestService { self: IssuesService =>
|
||||
import PullRequestService._
|
||||
|
||||
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] = {
|
||||
val issue = getIssue(owner, repository, issueId.toString)
|
||||
if(issue.isDefined){
|
||||
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match {
|
||||
case Some(pullreq) => Some((issue.get, pullreq))
|
||||
case None => None
|
||||
}
|
||||
} else None
|
||||
}
|
||||
|
||||
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
|
||||
Query(PullRequests)
|
||||
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.filter { case (t1, t2) =>
|
||||
(t2.closed is closed.bind) &&
|
||||
(t1.userName is owner.bind) &&
|
||||
(t1.repositoryName is repository.get.bind, repository.isDefined)
|
||||
}
|
||||
.groupBy { case (t1, t2) => t2.openedUserName }
|
||||
.map { case (userName, t) => userName ~ t.length }
|
||||
.sortBy(_._2 desc)
|
||||
.list
|
||||
.map { x => PullRequestCount(x._1, x._2) }
|
||||
|
||||
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
|
||||
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||
commitIdFrom: String, commitIdTo: String): Unit =
|
||||
PullRequests insert (PullRequest(
|
||||
originUserName,
|
||||
originRepositoryName,
|
||||
issueId,
|
||||
originBranch,
|
||||
requestUserName,
|
||||
requestRepositoryName,
|
||||
requestBranch,
|
||||
commitIdFrom,
|
||||
commitIdTo))
|
||||
|
||||
}
|
||||
|
||||
object PullRequestService {
|
||||
|
||||
val PullRequestLimit = 25
|
||||
|
||||
case class PullRequestCount(userName: String, count: Int)
|
||||
|
||||
}
|
||||
124
src/main/scala/service/RepositorySearchService.scala
Normal file
124
src/main/scala/service/RepositorySearchService.scala
Normal file
@@ -0,0 +1,124 @@
|
||||
package service
|
||||
|
||||
import util.{FileUtil, StringUtil, JGitUtil}
|
||||
import util.Directory._
|
||||
import model.Issue
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.treewalk.TreeWalk
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import org.eclipse.jgit.lib.FileMode
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
trait RepositorySearchService { self: IssuesService =>
|
||||
import RepositorySearchService._
|
||||
|
||||
def countIssues(owner: String, repository: String, query: String): Int =
|
||||
searchIssuesByKeyword(owner, repository, query).length
|
||||
|
||||
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
|
||||
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
||||
IssueSearchResult(
|
||||
issue.issueId,
|
||||
issue.title,
|
||||
issue.openedUserName,
|
||||
issue.registeredDate,
|
||||
commentCount,
|
||||
getHighlightText(content, query)._1)
|
||||
}
|
||||
|
||||
def countFiles(owner: String, repository: String, query: String): Int =
|
||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
||||
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
|
||||
}
|
||||
|
||||
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
|
||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
||||
if(JGitUtil.isEmpty(git)){
|
||||
Nil
|
||||
} else {
|
||||
val files = searchRepositoryFiles(git, query)
|
||||
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
|
||||
files.map { case (path, text) =>
|
||||
val (highlightText, lineNumber) = getHighlightText(text, query)
|
||||
FileSearchResult(
|
||||
path,
|
||||
commits(path).getCommitterIdent.getWhen,
|
||||
highlightText,
|
||||
lineNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
val objectId = git.getRepository.resolve("HEAD")
|
||||
val revCommit = revWalk.parseCommit(objectId)
|
||||
val treeWalk = new TreeWalk(git.getRepository)
|
||||
treeWalk.setRecursive(true)
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
val list = new ListBuffer[(String, String)]
|
||||
|
||||
while (treeWalk.next()) {
|
||||
if(treeWalk.getFileMode(0) != FileMode.TREE){
|
||||
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
|
||||
if(FileUtil.isText(bytes)){
|
||||
val text = StringUtil.convertFromByteArray(bytes)
|
||||
val lowerText = text.toLowerCase
|
||||
val indices = keywords.map(lowerText.indexOf _)
|
||||
if(!indices.exists(_ < 0)){
|
||||
list.append((treeWalk.getPathString, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
treeWalk.release
|
||||
revWalk.release
|
||||
|
||||
list.toList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object RepositorySearchService {
|
||||
|
||||
val CodeLimit = 10
|
||||
val IssueLimit = 10
|
||||
|
||||
def getHighlightText(content: String, query: String): (String, Int) = {
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
val lowerText = content.toLowerCase
|
||||
val indices = keywords.map(lowerText.indexOf _)
|
||||
|
||||
if(!indices.exists(_ < 0)){
|
||||
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
|
||||
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
|
||||
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
|
||||
"<span class=\"highlight\">$1</span>")
|
||||
(highlightText, lineNumber + 1)
|
||||
} else {
|
||||
(content.split("\n").take(5).mkString("\n"), 1)
|
||||
}
|
||||
}
|
||||
|
||||
case class SearchResult(
|
||||
files : List[(String, String)],
|
||||
issues: List[(Issue, Int, String)])
|
||||
|
||||
case class IssueSearchResult(
|
||||
issueId: Int,
|
||||
title: String,
|
||||
openedUserName: String,
|
||||
registeredDate: java.util.Date,
|
||||
commentCount: Int,
|
||||
highlightText: String)
|
||||
|
||||
case class FileSearchResult(
|
||||
path: String,
|
||||
lastModified: java.util.Date,
|
||||
highlightText: String,
|
||||
highlightLineNumber: Int)
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import Repositories._
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
import util.JGitUtil
|
||||
@@ -12,29 +11,37 @@ trait RepositoryService { self: AccountService =>
|
||||
/**
|
||||
* Creates a new repository.
|
||||
*
|
||||
* The project is created as public repository at first. Users can modify the project type at the repository settings
|
||||
* page after the project creation to configure the project as the private repository.
|
||||
*
|
||||
* @param repositoryName the repository name
|
||||
* @param userName the user name of the repository owner
|
||||
* @param description the repository description
|
||||
* @param isPrivate the repository type (private is true, otherwise false)
|
||||
* @param originRepositoryName specify for the forked repository. (default is None)
|
||||
* @param originUserName specify for the forked repository. (default is None)
|
||||
*/
|
||||
def createRepository(repositoryName: String, userName: String, description: Option[String]): Unit = {
|
||||
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
|
||||
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
|
||||
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = {
|
||||
Repositories insert
|
||||
Repository(
|
||||
userName = userName,
|
||||
repositoryName = repositoryName,
|
||||
isPrivate = false,
|
||||
description = description,
|
||||
defaultBranch = "master",
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastActivityDate = currentDate)
|
||||
|
||||
userName = userName,
|
||||
repositoryName = repositoryName,
|
||||
isPrivate = isPrivate,
|
||||
description = description,
|
||||
defaultBranch = "master",
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastActivityDate = currentDate,
|
||||
originUserName = originUserName,
|
||||
originRepositoryName = originRepositoryName,
|
||||
parentUserName = parentUserName,
|
||||
parentRepositoryName = parentRepositoryName)
|
||||
|
||||
IssueId insert (userName, repositoryName, 0)
|
||||
}
|
||||
|
||||
def deleteRepository(userName: String, repositoryName: String): Unit = {
|
||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
@@ -54,40 +61,6 @@ trait RepositoryService { self: AccountService =>
|
||||
def getRepositoryNamesOfUser(userName: String): List[String] =
|
||||
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
|
||||
|
||||
/**
|
||||
* Returns the list of specified user's repositories information.
|
||||
*
|
||||
* @param userName the user name
|
||||
* @param baseUrl the base url of this application
|
||||
* @param loginUserName the logged in user name
|
||||
* @return the list of repository information which is sorted in descending order of lastActivityDate.
|
||||
*/
|
||||
def getVisibleRepositories(userName: String, baseUrl: String, loginUserName: Option[String]): List[RepositoryInfo] = {
|
||||
val q1 = Repositories
|
||||
.filter { t => t.userName is userName.bind }
|
||||
.map { r => r }
|
||||
|
||||
val q2 = Collaborators
|
||||
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||
.filter{ case (t1, t2) => t1.collaboratorName is userName.bind}
|
||||
.map { case (t1, t2) => t2 }
|
||||
|
||||
def visibleFor(t: Repositories.type, loginUserName: Option[String]) = {
|
||||
loginUserName match {
|
||||
case Some(x) => (t.isPrivate is false.bind) || (
|
||||
(t.isPrivate is true.bind) && ((t.userName is x.bind) || (Collaborators.filter { c =>
|
||||
c.byRepository(t.userName, t.repositoryName) && (c.collaboratorName is x.bind)
|
||||
}.exists)))
|
||||
case None => (t.isPrivate is false.bind)
|
||||
}
|
||||
}
|
||||
|
||||
q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
|
||||
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified repository information.
|
||||
*
|
||||
@@ -98,36 +71,69 @@ trait RepositoryService { self: AccountService =>
|
||||
*/
|
||||
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
|
||||
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
|
||||
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
|
||||
// for getting issue count and pull request count
|
||||
val issues = Query(Issues).filter { t =>
|
||||
t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
|
||||
}.map(_.pullRequest).list
|
||||
|
||||
new RepositoryInfo(
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
||||
repository,
|
||||
issues.size,
|
||||
issues.filter(_ == true).size,
|
||||
getForkedCount(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
|
||||
Query(Repositories).filter { t1 =>
|
||||
(t1.userName is userName.bind) ||
|
||||
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
|
||||
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||
new RepositoryInfo(
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
||||
repository,
|
||||
getForkedCount(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of accessible repositories information for the specified account user.
|
||||
*
|
||||
* @param account the account
|
||||
* Returns the list of visible repositories for the specified user.
|
||||
* If repositoryUserName is given then filters results by repository owner.
|
||||
*
|
||||
* @param loginAccount the logged in account
|
||||
* @param baseUrl the base url of this application
|
||||
* @return the repository informations which is sorted in descending order of lastActivityDate.
|
||||
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
|
||||
* @return the repository information which is sorted in descending order of lastActivityDate.
|
||||
*/
|
||||
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = {
|
||||
|
||||
def createRepositoryInfo(repository: Repository): RepositoryInfo = {
|
||||
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
|
||||
}
|
||||
|
||||
(account match {
|
||||
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
|
||||
(loginAccount match {
|
||||
// for Administrators
|
||||
case Some(x) if(x.isAdmin) => Query(Repositories)
|
||||
// for Normal Users
|
||||
case Some(x) if(!x.isAdmin) =>
|
||||
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
|
||||
(Query(Collaborators).filter(t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)) exists)
|
||||
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
|
||||
}
|
||||
// for Guests
|
||||
case None => Query(Repositories) filter(_.isPrivate is false.bind)
|
||||
}).sortBy(_.lastActivityDate desc).list.map(createRepositoryInfo _)
|
||||
}).filter { t =>
|
||||
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
|
||||
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||
new RepositoryInfo(
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
||||
repository,
|
||||
getForkedCount(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +171,15 @@ trait RepositoryService { self: AccountService =>
|
||||
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
|
||||
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
|
||||
|
||||
/**
|
||||
* Remove all collaborators from the repository.
|
||||
*
|
||||
* @param userName the user name of the repository owner
|
||||
* @param repositoryName the repository name
|
||||
*/
|
||||
def removeCollaborators(userName: String, repositoryName: String): Unit =
|
||||
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
|
||||
|
||||
/**
|
||||
* Returns the list of collaborators name which is sorted with ascending order.
|
||||
*
|
||||
@@ -184,11 +199,42 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
}
|
||||
|
||||
// TODO It must be _.length instead of map (_.issueId) list).length.
|
||||
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
|
||||
// https://github.com/slick/slick/issues/170
|
||||
private def getForkedCount(userName: String, repositoryName: String): Int =
|
||||
Query(Repositories).filter { t =>
|
||||
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
|
||||
}.list.length
|
||||
|
||||
|
||||
def getForkedRepositories(userName: String, repositoryName: String): List[String] =
|
||||
Query(Repositories).filter { t =>
|
||||
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
|
||||
}
|
||||
.sortBy(_.userName asc).map(_.userName).list
|
||||
|
||||
}
|
||||
|
||||
object RepositoryService {
|
||||
|
||||
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
|
||||
branchList: List[String], tags: List[util.JGitUtil.TagInfo])
|
||||
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
|
||||
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
|
||||
|
||||
/**
|
||||
* Creates instance with issue count and pull request count.
|
||||
*/
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
|
||||
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
|
||||
|
||||
/**
|
||||
* Creates instance without issue count and pull request count.
|
||||
*/
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
|
||||
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
|
||||
}
|
||||
|
||||
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
|
||||
|
||||
}
|
||||
32
src/main/scala/service/RequestCache.scala
Normal file
32
src/main/scala/service/RequestCache.scala
Normal file
@@ -0,0 +1,32 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import service.SystemSettingsService.SystemSettings
|
||||
|
||||
/**
|
||||
* This service is used for a view helper mainly.
|
||||
*
|
||||
* It may be called many times in one request, so each method stores
|
||||
* its result into the cache which available during a request.
|
||||
*/
|
||||
trait RequestCache {
|
||||
|
||||
def getSystemSettings()(implicit context: app.Context): SystemSettings =
|
||||
context.cache("system_settings"){
|
||||
new SystemSettingsService {}.loadSystemSettings()
|
||||
}
|
||||
|
||||
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
|
||||
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
|
||||
new IssuesService {}.getIssue(userName, repositoryName, issueId)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
|
||||
context.cache(s"account.${userName}"){
|
||||
new AccountService {}.getAccountByUserName(userName)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,40 +1,141 @@
|
||||
package service
|
||||
|
||||
import util.Directory._
|
||||
import SystemSettingsService._
|
||||
|
||||
trait SystemSettingsService {
|
||||
|
||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||
val props = new java.util.Properties()
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.store(new java.io.FileOutputStream(GitBucketConf), null)
|
||||
}
|
||||
|
||||
|
||||
def loadSystemSettings(): SystemSettings = {
|
||||
val props = new java.util.Properties()
|
||||
if(GitBucketConf.exists){
|
||||
props.load(new java.io.FileInputStream(GitBucketConf))
|
||||
}
|
||||
SystemSettings(getBoolean(props, "allow_account_registration"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object SystemSettingsService {
|
||||
|
||||
case class SystemSettings(allowAccountRegistration: Boolean)
|
||||
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
|
||||
private def getBoolean(props: java.util.Properties, key: String, default: Boolean = false): Boolean = {
|
||||
val value = props.getProperty(key)
|
||||
if(value == null || value.isEmpty){
|
||||
default
|
||||
} else {
|
||||
value.toBoolean
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
package service
|
||||
|
||||
import util.Directory._
|
||||
import SystemSettingsService._
|
||||
|
||||
trait SystemSettingsService {
|
||||
|
||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||
val props = new java.util.Properties()
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
if(settings.notification) {
|
||||
settings.smtp.foreach { smtp =>
|
||||
props.setProperty(SmtpHost, smtp.host)
|
||||
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
||||
smtp.user.foreach(props.setProperty(SmtpUser, _))
|
||||
smtp.password.foreach(props.setProperty(SmtpPassword, _))
|
||||
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
||||
}
|
||||
}
|
||||
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
|
||||
if(settings.ldapAuthentication){
|
||||
settings.ldap.map { ldap =>
|
||||
props.setProperty(LdapHost, ldap.host)
|
||||
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
|
||||
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
|
||||
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
|
||||
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
|
||||
}
|
||||
}
|
||||
props.store(new java.io.FileOutputStream(GitBucketConf), null)
|
||||
}
|
||||
|
||||
|
||||
def loadSystemSettings(): SystemSettings = {
|
||||
val props = new java.util.Properties()
|
||||
if(GitBucketConf.exists){
|
||||
props.load(new java.io.FileInputStream(GitBucketConf))
|
||||
}
|
||||
SystemSettings(
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, Gravatar, true),
|
||||
getValue(props, Notification, false),
|
||||
if(getValue(props, Notification, false)){
|
||||
Some(Smtp(
|
||||
getValue(props, SmtpHost, ""),
|
||||
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
||||
getOptionValue(props, SmtpUser, None),
|
||||
getOptionValue(props, SmtpPassword, None),
|
||||
getOptionValue[Boolean](props, SmtpSsl, None)))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
getValue(props, LdapAuthentication, false),
|
||||
if(getValue(props, LdapAuthentication, false)){
|
||||
Some(Ldap(
|
||||
getValue(props, LdapHost, ""),
|
||||
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
|
||||
getOptionValue(props, LdapBindDN, None),
|
||||
getOptionValue(props, LdapBindPassword, None),
|
||||
getValue(props, LdapBaseDN, ""),
|
||||
getValue(props, LdapUserNameAttribute, ""),
|
||||
getValue(props, LdapMailAddressAttribute, "")))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object SystemSettingsService {
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
case class SystemSettings(
|
||||
allowAccountRegistration: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap])
|
||||
|
||||
case class Ldap(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
bindDN: Option[String],
|
||||
bindPassword: Option[String],
|
||||
baseDN: String,
|
||||
userNameAttribute: String,
|
||||
mailAttribute: String)
|
||||
|
||||
case class Smtp(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
user: Option[String],
|
||||
password: Option[String],
|
||||
ssl: Option[Boolean])
|
||||
|
||||
val DefaultSmtpPort = 25
|
||||
val DefaultLdapPort = 389
|
||||
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
private val SmtpHost = "smtp.host"
|
||||
private val SmtpPort = "smtp.port"
|
||||
private val SmtpUser = "smtp.user"
|
||||
private val SmtpPassword = "smtp.password"
|
||||
private val SmtpSsl = "smtp.ssl"
|
||||
private val LdapAuthentication = "ldap_authentication"
|
||||
private val LdapHost = "ldap.host"
|
||||
private val LdapPort = "ldap.port"
|
||||
private val LdapBindDN = "ldap.bindDN"
|
||||
private val LdapBindPassword = "ldap.bind_password"
|
||||
private val LdapBaseDN = "ldap.baseDN"
|
||||
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
|
||||
val value = props.getProperty(key)
|
||||
if(value == null || value.isEmpty) default
|
||||
else convertType(value).asInstanceOf[A]
|
||||
}
|
||||
|
||||
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = {
|
||||
val value = props.getProperty(key)
|
||||
if(value == null || value.isEmpty) default
|
||||
else Some(convertType(value)).asInstanceOf[Option[A]]
|
||||
}
|
||||
|
||||
private def convertType[A: ClassTag](value: String) = {
|
||||
val c = implicitly[ClassTag[A]].runtimeClass
|
||||
if(c == classOf[Boolean]) value.toBoolean
|
||||
else if(c == classOf[Int]) value.toInt
|
||||
else value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@ import java.io.File
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.JGitUtil.DiffInfo
|
||||
import util.{Directory, JGitUtil}
|
||||
import org.eclipse.jgit.lib.RepositoryBuilder
|
||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import util.{Directory, JGitUtil, LockUtil}
|
||||
|
||||
object WikiService {
|
||||
|
||||
@@ -32,50 +28,21 @@ object WikiService {
|
||||
*/
|
||||
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
||||
|
||||
/**
|
||||
* lock objects
|
||||
*/
|
||||
private val locks = new ConcurrentHashMap[String, AnyRef]()
|
||||
|
||||
/**
|
||||
* Returns the lock object for the specified repository.
|
||||
*/
|
||||
private def getLockObject(owner: String, repository: String): AnyRef = synchronized {
|
||||
val key = owner + "/" + repository
|
||||
if(!locks.containsKey(key)){
|
||||
locks.put(key, new AnyRef())
|
||||
}
|
||||
locks.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes a given function which modifies the working copy of the wiki repository.
|
||||
*
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param f the function which modifies the working copy of the wiki repository
|
||||
* @tparam T the return type of the given function
|
||||
* @return the result of the given function
|
||||
*/
|
||||
def lock[T](owner: String, repository: String)(f: => T): T = getLockObject(owner, repository).synchronized(f)
|
||||
|
||||
}
|
||||
|
||||
trait WikiService {
|
||||
import WikiService._
|
||||
|
||||
def createWikiRepository(owner: model.Account, repository: String): Unit = {
|
||||
lock(owner.userName, repository){
|
||||
val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
|
||||
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
val dir = Directory.getWikiRepositoryDir(owner, repository)
|
||||
if(!dir.exists){
|
||||
val repo = new RepositoryBuilder().setGitDir(dir).setBare.build
|
||||
try {
|
||||
repo.create
|
||||
saveWikiPage(owner.userName, repository, "Home", "Home", "Welcome to the %s wiki!!".format(repository), owner, "Initial Commit")
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
|
||||
} finally {
|
||||
repo.close
|
||||
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
|
||||
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository))
|
||||
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,14 +53,11 @@ trait WikiService {
|
||||
*/
|
||||
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
||||
try {
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
||||
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
|
||||
}
|
||||
} catch {
|
||||
// TODO no commit, but it should not judge by exception.
|
||||
case e: NullPointerException => None
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +66,7 @@ trait WikiService {
|
||||
*/
|
||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = {
|
||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
||||
try {
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
val index = path.lastIndexOf('/')
|
||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||
val fileName = if(index < 0) path else path.substring(index + 1)
|
||||
@@ -110,10 +74,7 @@ trait WikiService {
|
||||
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
||||
git.getRepository.open(file.id).getBytes
|
||||
}
|
||||
} catch {
|
||||
// TODO no commit, but it should not judge by exception.
|
||||
case e: NullPointerException => None
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,19 +82,21 @@ trait WikiService {
|
||||
* Returns the list of wiki page names.
|
||||
*/
|
||||
def getWikiPageList(owner: String, repository: String): List[String] = {
|
||||
JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
|
||||
.filter(_.name.endsWith(".md"))
|
||||
.map(_.name.replaceFirst("\\.md$", ""))
|
||||
.sortBy(x => x)
|
||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
||||
JGitUtil.getFileList(git, "master", ".")
|
||||
.filter(_.name.endsWith(".md"))
|
||||
.map(_.name.replaceFirst("\\.md$", ""))
|
||||
.sortBy(x => x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the wiki page.
|
||||
*/
|
||||
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
||||
content: String, committer: model.Account, message: String): Unit = {
|
||||
content: String, committer: model.Account, message: String): Option[String] = {
|
||||
|
||||
lock(owner, repository){
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
// clone working copy
|
||||
val workDir = Directory.getWikiWorkDir(owner, repository)
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
@@ -159,8 +122,11 @@ trait WikiService {
|
||||
|
||||
// commit and push
|
||||
if(added || deleted){
|
||||
git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
|
||||
val commit = git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
|
||||
git.push.call
|
||||
Some(commit.getName)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,8 +135,9 @@ trait WikiService {
|
||||
/**
|
||||
* Delete the wiki page.
|
||||
*/
|
||||
def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = {
|
||||
lock(owner, repository){
|
||||
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||
committer: String, mailAddress: String, message: String): Unit = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
// clone working copy
|
||||
val workDir = Directory.getWikiWorkDir(owner, repository)
|
||||
cloneOrPullWorkingCopy(workDir, owner, repository)
|
||||
@@ -182,42 +149,24 @@ trait WikiService {
|
||||
git.rm.addFilepattern(pageName + ".md").call
|
||||
|
||||
// commit and push
|
||||
// TODO committer's mail address
|
||||
git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call
|
||||
git.commit.setAuthor(committer, mailAddress).setMessage(message).call
|
||||
git.push.call
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns differences between specified commits.
|
||||
*/
|
||||
def getWikiDiffs(git: Git, commitId1: String, commitId2: String): List[DiffInfo] = {
|
||||
// get diff between specified commit and its previous commit
|
||||
val reader = git.getRepository.newObjectReader
|
||||
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(commitId1 + "^{tree}"))
|
||||
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(commitId2 + "^{tree}"))
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
||||
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).map(new String(_, "UTF-8")),
|
||||
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).map(new String(_, "UTF-8")))
|
||||
}.toList
|
||||
}
|
||||
|
||||
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
|
||||
if(!workDir.exists){
|
||||
Git.cloneRepository
|
||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
||||
.setDirectory(workDir)
|
||||
.call
|
||||
val git =
|
||||
Git.cloneRepository
|
||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
||||
.setDirectory(workDir)
|
||||
.call
|
||||
git.getRepository.close // close .git resources.
|
||||
} else {
|
||||
Git.open(workDir).pull.call
|
||||
JGitUtil.withGit(workDir){ git =>
|
||||
git.pull.call
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ object AutoUpdate {
|
||||
* If corresponding SQL file does not exist, this method do nothing.
|
||||
*/
|
||||
def update(conn: Connection): Unit = {
|
||||
val sqlPath = "update/%d_%d.sql".format(majorVersion, minorVersion)
|
||||
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
|
||||
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
|
||||
if(in != null){
|
||||
val sql = IOUtils.toString(in, "UTF-8")
|
||||
@@ -42,15 +42,35 @@ object AutoUpdate {
|
||||
/**
|
||||
* MAJOR.MINOR
|
||||
*/
|
||||
val versionString = "%d.%d".format(majorVersion, minorVersion)
|
||||
val versionString = s"${majorVersion}.${minorVersion}"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
Version(1, 1),
|
||||
Version(1, 0)
|
||||
Version(1, 5),
|
||||
Version(1, 4),
|
||||
new Version(1, 3){
|
||||
override def update(conn: Connection): Unit = {
|
||||
super.update(conn)
|
||||
// Fix wiki repository configuration
|
||||
val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")
|
||||
while(rs.next){
|
||||
val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))
|
||||
val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository
|
||||
val config = repository.getConfig
|
||||
if(!config.getBoolean("http", "receivepack", false)){
|
||||
config.setBoolean("http", null, "receivepack", true)
|
||||
config.save
|
||||
}
|
||||
repository.close
|
||||
}
|
||||
}
|
||||
},
|
||||
Version(1, 2),
|
||||
Version(1, 1),
|
||||
Version(1, 0)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -92,6 +112,7 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
|
||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||
|
||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${Directory.DatabaseHome}")
|
||||
super.contextInitialized(event)
|
||||
logger.debug("H2 started")
|
||||
|
||||
@@ -110,6 +131,7 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
|
||||
} catch {
|
||||
case ex: Throwable => {
|
||||
logger.error("Failed to schema update", ex)
|
||||
ex.printStackTrace()
|
||||
conn.rollback()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package servlet
|
||||
|
||||
import javax.servlet._
|
||||
import javax.servlet.http._
|
||||
import util.StringUtil._
|
||||
import service.{AccountService, RepositoryService}
|
||||
import service.{SystemSettingsService, AccountService, RepositoryService}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
|
||||
*/
|
||||
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService {
|
||||
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
|
||||
|
||||
@@ -21,6 +20,10 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
val request = req.asInstanceOf[HttpServletRequest]
|
||||
val response = res.asInstanceOf[HttpServletResponse]
|
||||
|
||||
val wrappedResponse = new HttpServletResponseWrapper(response){
|
||||
override def setCharacterEncoding(encoding: String) = {}
|
||||
}
|
||||
|
||||
try {
|
||||
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
|
||||
val repositoryOwner = paths(2)
|
||||
@@ -28,15 +31,16 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
|
||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
|
||||
case Some(repository) => {
|
||||
if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){
|
||||
chain.doFilter(req, res)
|
||||
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
|
||||
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
} else {
|
||||
request.getHeader("Authorization") match {
|
||||
case null => requireAuth(response)
|
||||
case auth => decodeAuthHeader(auth).split(":") match {
|
||||
case Array(username, password) if(isWritableUser(username, password, repository)) => {
|
||||
request.setAttribute("USER_NAME", username)
|
||||
chain.doFilter(req, res)
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
}
|
||||
case _ => requireAuth(response)
|
||||
}
|
||||
@@ -53,12 +57,12 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
}
|
||||
}
|
||||
|
||||
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = {
|
||||
getAccountByUserName(username).map { account =>
|
||||
account.password == encrypt(password) && hasWritePermission(repository.owner, repository.name, Some(account))
|
||||
} getOrElse false
|
||||
}
|
||||
|
||||
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
|
||||
authenticate(loadSystemSettings(), username, password) match {
|
||||
case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
|
||||
case None => false
|
||||
}
|
||||
|
||||
private def requireAuth(response: HttpServletResponse): Unit = {
|
||||
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
||||
|
||||
@@ -24,7 +24,7 @@ class GitRepositoryServlet extends GitServlet {
|
||||
|
||||
override def init(config: ServletConfig): Unit = {
|
||||
setReceivePackFactory(new GitBucketReceivePackFactory())
|
||||
|
||||
|
||||
// TODO are there any other ways...?
|
||||
super.init(new ServletConfig(){
|
||||
def getInitParameter(name: String): String = name match {
|
||||
@@ -33,12 +33,14 @@ class GitRepositoryServlet extends GitServlet {
|
||||
case name => config.getInitParameter(name)
|
||||
}
|
||||
def getInitParameterNames(): java.util.Enumeration[String] = {
|
||||
config.getInitParameterNames
|
||||
config.getInitParameterNames
|
||||
}
|
||||
|
||||
|
||||
def getServletContext(): ServletContext = config.getServletContext
|
||||
def getServletName(): String = config.getServletName
|
||||
});
|
||||
|
||||
super.init(config)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,7 +51,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
|
||||
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
|
||||
val receivePack = new ReceivePack(db)
|
||||
val userName = request.getAttribute("USER_NAME")
|
||||
val userName = request.getAttribute("USER_NAME").asInstanceOf[String]
|
||||
|
||||
logger.debug("requestURI: " + request.getRequestURI)
|
||||
logger.debug("userName:" + userName)
|
||||
@@ -60,27 +62,52 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
|
||||
logger.debug("repository:" + owner + "/" + repository)
|
||||
|
||||
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository))
|
||||
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName))
|
||||
receivePack
|
||||
}
|
||||
}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
class CommitLogHook(owner: String, repository: String) extends PostReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService {
|
||||
class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||
|
||||
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git =>
|
||||
commands.asScala.foreach { command =>
|
||||
JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name).foreach { commit =>
|
||||
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
|
||||
val issueId = matchData.group(2)
|
||||
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
|
||||
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, None)
|
||||
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
|
||||
val refName = command.getRefName.split("/")
|
||||
|
||||
// apply issue comment
|
||||
val newCommits = commits.flatMap { commit =>
|
||||
if(!existsCommitId(owner, repository, commit.id)){
|
||||
insertCommitId(owner, repository, commit.id)
|
||||
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
|
||||
val issueId = matchData.group(2)
|
||||
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
|
||||
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit")
|
||||
}
|
||||
}
|
||||
Some(commit)
|
||||
} else None
|
||||
}.toList
|
||||
|
||||
// record activity
|
||||
if(refName(1) == "heads"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => {
|
||||
recordCreateBranchActivity(owner, repository, userName, refName(2))
|
||||
recordPushActivity(owner, repository, userName, refName(2), newCommits)
|
||||
}
|
||||
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, refName(2), newCommits)
|
||||
case _ =>
|
||||
}
|
||||
} else if(refName(1) == "tags"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/main/scala/servlet/SessionCleanupListener.scala
Normal file
15
src/main/scala/servlet/SessionCleanupListener.scala
Normal file
@@ -0,0 +1,15 @@
|
||||
package servlet
|
||||
|
||||
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
|
||||
import app.FileUploadControllerBase
|
||||
|
||||
/**
|
||||
* Removes session associated temporary files when session is destroyed.
|
||||
*/
|
||||
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
|
||||
|
||||
def sessionCreated(se: HttpSessionEvent): Unit = {}
|
||||
|
||||
def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
|
||||
|
||||
}
|
||||
@@ -3,10 +3,9 @@ package servlet
|
||||
import javax.servlet._
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import scala.slick.session.Database
|
||||
|
||||
/**
|
||||
* Controls the transaction with the open session in view pattern.
|
||||
* Controls the transaction with the open session in view pattern.
|
||||
*/
|
||||
class TransactionFilter extends Filter {
|
||||
|
||||
@@ -21,16 +20,19 @@ class TransactionFilter extends Filter {
|
||||
// assets don't need transaction
|
||||
chain.doFilter(req, res)
|
||||
} else {
|
||||
// TODO begin transaction!
|
||||
val context = req.getServletContext
|
||||
Database.forURL(context.getInitParameter("db.url"),
|
||||
context.getInitParameter("db.user"),
|
||||
context.getInitParameter("db.password")) withTransaction {
|
||||
Database(req.getServletContext) withTransaction {
|
||||
logger.debug("TODO begin transaction")
|
||||
chain.doFilter(req, res)
|
||||
logger.debug("TODO end transaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Database {
|
||||
def apply(context: ServletContext): scala.slick.session.Database =
|
||||
scala.slick.session.Database.forURL(context.getInitParameter("db.url"),
|
||||
context.getInitParameter("db.user"),
|
||||
context.getInitParameter("db.password"))
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
package util
|
||||
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
|
||||
/**
|
||||
* Provides directories used by GitBucket.
|
||||
*/
|
||||
object Directory {
|
||||
|
||||
val GitBucketHome = new File(System.getProperty("user.home"), "gitbucket").getAbsolutePath
|
||||
val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
|
||||
case Some(env) => new File(env)
|
||||
case None => new File(System.getProperty("user.home"), "gitbucket")
|
||||
}).getAbsolutePath
|
||||
|
||||
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
||||
|
||||
val RepositoryHome = "%s/repositories".format(GitBucketHome)
|
||||
val RepositoryHome = s"${GitBucketHome}/repositories"
|
||||
|
||||
val DatabaseHome = s"${GitBucketHome}/data"
|
||||
|
||||
/**
|
||||
* Repository names of the specified user.
|
||||
*/
|
||||
def getRepositories(owner: String): List[String] = {
|
||||
val dir = new File("%s/%s".format(RepositoryHome, owner))
|
||||
val dir = new File(s"${RepositoryHome}/${owner}")
|
||||
if(dir.exists){
|
||||
dir.listFiles.filter { file =>
|
||||
file.isDirectory && !file.getName.endsWith(".wiki.git")
|
||||
@@ -33,19 +36,24 @@ object Directory {
|
||||
* Substance directory of the repository.
|
||||
*/
|
||||
def getRepositoryDir(owner: String, repository: String): File =
|
||||
new File("%s/%s/%s.git".format(RepositoryHome, owner, repository))
|
||||
new File(s"${RepositoryHome}/${owner}/${repository}.git")
|
||||
|
||||
/**
|
||||
* Directory for uploaded files by the specified user.
|
||||
*/
|
||||
def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
|
||||
|
||||
/**
|
||||
* Root of temporary directories for the specified repository.
|
||||
*/
|
||||
def getTemporaryDir(owner: String, repository: String): File =
|
||||
new File("%s/tmp/%s/%s".format(GitBucketHome, owner, repository))
|
||||
new File(s"${GitBucketHome}/tmp/${owner}/${repository}")
|
||||
|
||||
/**
|
||||
* Temporary directory which is used to create an archive to download repository contents.
|
||||
*/
|
||||
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
|
||||
new File(getTemporaryDir(owner, repository), "download/%s".format(sessionId))
|
||||
new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
|
||||
|
||||
/**
|
||||
* Temporary directory which is used in the repository creation.
|
||||
@@ -60,7 +68,7 @@ object Directory {
|
||||
* Substance directory of the wiki repository.
|
||||
*/
|
||||
def getWikiRepositoryDir(owner: String, repository: String): File =
|
||||
new File("%s/%s/%s.wiki.git".format(Directory.RepositoryHome, owner, repository))
|
||||
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
|
||||
|
||||
/**
|
||||
* Wiki working directory which is cloned from the wiki repository.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package util
|
||||
|
||||
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
|
||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||
import java.net.URLConnection
|
||||
import java.io.File
|
||||
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
|
||||
@@ -44,4 +44,13 @@ object FileUtil {
|
||||
}
|
||||
}
|
||||
|
||||
def getExtension(name: String): String = {
|
||||
val index = name.lastIndexOf('.')
|
||||
if(index >= 0){
|
||||
name.substring(index + 1)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
package util
|
||||
|
||||
import twirl.api.Html
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/**
|
||||
* Provides some usable implicit conversions.
|
||||
*/
|
||||
object Implicits {
|
||||
|
||||
implicit def extendsSeq[A](seq: Seq[A]) = new {
|
||||
|
||||
implicit class RichSeq[A](seq: Seq[A]) {
|
||||
|
||||
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
|
||||
|
||||
@@ -25,9 +24,23 @@ object Implicits {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Should this implicit conversion move to model.Functions?
|
||||
implicit def extendsColumn(c1: Column[Boolean]) = new {
|
||||
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
|
||||
implicit class RichString(value: String){
|
||||
def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
|
||||
val sb = new StringBuilder()
|
||||
var i = 0
|
||||
regex.findAllIn(value).matchData.foreach { m =>
|
||||
sb.append(value.substring(i, m.start))
|
||||
i = m.end
|
||||
replace(m) match {
|
||||
case Some(s) => sb.append(s)
|
||||
case None => sb.append(m.matched)
|
||||
}
|
||||
}
|
||||
if(i < value.length){
|
||||
sb.append(value.substring(i))
|
||||
}
|
||||
sb.toString
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,18 +2,18 @@ package util
|
||||
|
||||
import org.eclipse.jgit.api.Git
|
||||
import util.Directory._
|
||||
import util.StringUtil._
|
||||
import scala.collection.JavaConverters._
|
||||
import javax.servlet.ServletContext
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.revwalk._
|
||||
import org.eclipse.jgit.revwalk.filter._
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import org.eclipse.jgit.treewalk.filter._
|
||||
import org.eclipse.jgit.diff._
|
||||
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||
import org.eclipse.jgit.util.io.DisabledOutputStream
|
||||
import org.eclipse.jgit.errors.MissingObjectException
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.errors.NoHeadException
|
||||
import service.RepositoryService
|
||||
|
||||
/**
|
||||
* Provides complex JGit operations.
|
||||
@@ -26,10 +26,11 @@ object JGitUtil {
|
||||
* @param owner the user name of the repository owner
|
||||
* @param name the repository name
|
||||
* @param url the repository URL
|
||||
* @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001.
|
||||
* @param branchList the list of branch names
|
||||
* @param tags the list of tags
|
||||
*/
|
||||
case class RepositoryInfo(owner: String, name: String, url: String, branchList: List[String], tags: List[TagInfo])
|
||||
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo])
|
||||
|
||||
/**
|
||||
* The file data for the file list of the repository viewer.
|
||||
@@ -50,14 +51,21 @@ object JGitUtil {
|
||||
* @param id the commit id
|
||||
* @param time the commit time
|
||||
* @param committer the committer name
|
||||
* @param mailAddress the mail address of the committer
|
||||
* @param shortMessage the short message
|
||||
* @param fullMessage the full message
|
||||
* @param parents the list of parent commit id
|
||||
*/
|
||||
case class CommitInfo(id: String, time: Date, committer: String, shortMessage: String, fullMessage: String, parents: List[String]){
|
||||
case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
|
||||
shortMessage: String, fullMessage: String, parents: List[String]){
|
||||
|
||||
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
|
||||
rev.getName, rev.getCommitterIdent.getWhen, rev.getCommitterIdent.getName, rev.getShortMessage, rev.getFullMessage,
|
||||
rev.getName,
|
||||
rev.getCommitterIdent.getWhen,
|
||||
rev.getCommitterIdent.getName,
|
||||
rev.getCommitterIdent.getEmailAddress,
|
||||
rev.getShortMessage,
|
||||
rev.getFullMessage,
|
||||
rev.getParents().map(_.name).toList)
|
||||
|
||||
val summary = {
|
||||
@@ -123,15 +131,18 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns RevCommit from the commit id.
|
||||
* Returns RevCommit from the commit or tag id.
|
||||
*
|
||||
* @param git the Git object
|
||||
* @param commitId the ObjectId of the commit
|
||||
* @return the RevCommit for the specified commit
|
||||
* @param objectId the ObjectId of the commit or tag
|
||||
* @return the RevCommit for the specified commit or tag
|
||||
*/
|
||||
def getRevCommitFromId(git: Git, commitId: ObjectId): RevCommit = {
|
||||
def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = {
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
val revCommit = revWalk.parseCommit(commitId)
|
||||
val revCommit = revWalk.parseAny(objectId) match {
|
||||
case r: RevTag => revWalk.parseCommit(r.getObject)
|
||||
case _ => revWalk.parseCommit(objectId)
|
||||
}
|
||||
revWalk.dispose
|
||||
revCommit
|
||||
}
|
||||
@@ -141,18 +152,30 @@ object JGitUtil {
|
||||
*/
|
||||
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
|
||||
withGit(getRepositoryDir(owner, repository)){ git =>
|
||||
RepositoryInfo(
|
||||
owner, repository, baseUrl + "/git/%s/%s.git".format(owner, repository),
|
||||
// branches
|
||||
git.branchList.call.asScala.map { ref =>
|
||||
ref.getName.replaceFirst("^refs/heads/", "")
|
||||
}.toList,
|
||||
// tags
|
||||
git.tagList.call.asScala.map { ref =>
|
||||
val revCommit = getRevCommitFromId(git, ref.getObjectId)
|
||||
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
|
||||
}.toList
|
||||
)
|
||||
try {
|
||||
// get commit count
|
||||
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
|
||||
|
||||
RepositoryInfo(
|
||||
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
|
||||
// commit count
|
||||
commitCount,
|
||||
// branches
|
||||
git.branchList.call.asScala.map { ref =>
|
||||
ref.getName.replaceFirst("^refs/heads/", "")
|
||||
}.toList,
|
||||
// tags
|
||||
git.tagList.call.asScala.map { ref =>
|
||||
val revCommit = getRevCommitFromId(git, ref.getObjectId)
|
||||
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
|
||||
}.toList
|
||||
)
|
||||
} catch {
|
||||
// not initialized
|
||||
case e: NoHeadException => RepositoryInfo(
|
||||
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +255,9 @@ object JGitUtil {
|
||||
* @param page the page number (1-)
|
||||
* @param limit the number of commit info per page. 0 (default) means unlimited.
|
||||
* @param path filters by this path. default is no filter.
|
||||
* @return a tuple of the commit list and whether has next
|
||||
* @return a tuple of the commit list and whether has next, or the error message
|
||||
*/
|
||||
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): (List[CommitInfo], Boolean) = {
|
||||
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = {
|
||||
val fixedPage = if(page <= 0) 1 else page
|
||||
|
||||
@scala.annotation.tailrec
|
||||
@@ -248,21 +271,52 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
|
||||
if(path.nonEmpty){
|
||||
revWalk.setRevFilter(new RevFilter(){
|
||||
def include(walk: RevWalk, commit: RevCommit): Boolean = {
|
||||
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty
|
||||
}
|
||||
override def clone(): RevFilter = this
|
||||
})
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
if(objectId == null){
|
||||
Left(s"${revision} can't be resolved.")
|
||||
} else {
|
||||
revWalk.markStart(revWalk.parseCommit(objectId))
|
||||
if(path.nonEmpty){
|
||||
revWalk.setRevFilter(new RevFilter(){
|
||||
def include(walk: RevWalk, commit: RevCommit): Boolean = {
|
||||
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
|
||||
}
|
||||
override def clone(): RevFilter = this
|
||||
})
|
||||
}
|
||||
|
||||
val commits = getCommitLog(revWalk.iterator, 0, Nil)
|
||||
revWalk.release
|
||||
|
||||
Right(commits)
|
||||
}
|
||||
|
||||
val commits = getCommitLog(revWalk.iterator, 0, Nil)
|
||||
revWalk.release
|
||||
|
||||
commits
|
||||
}
|
||||
|
||||
def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false)
|
||||
(endCondition: RevCommit => Boolean): List[CommitInfo] = {
|
||||
@scala.annotation.tailrec
|
||||
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
|
||||
i.hasNext match {
|
||||
case true => {
|
||||
val revCommit = i.next
|
||||
if(endCondition(revCommit)){
|
||||
if(includesLastCommit) logs :+ new CommitInfo(revCommit) else logs
|
||||
} else {
|
||||
getCommitLog(i, logs :+ new CommitInfo(revCommit))
|
||||
}
|
||||
}
|
||||
case false => logs
|
||||
}
|
||||
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
|
||||
|
||||
val commits = getCommitLog(revWalk.iterator, Nil)
|
||||
revWalk.release
|
||||
|
||||
commits.reverse
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the commit list between two revisions.
|
||||
@@ -272,30 +326,9 @@ object JGitUtil {
|
||||
* @param to the to revision
|
||||
* @return the commit list
|
||||
*/
|
||||
def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = {
|
||||
@scala.annotation.tailrec
|
||||
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
|
||||
i.hasNext match {
|
||||
case true => {
|
||||
val revCommit = i.next
|
||||
if(revCommit.name == from){
|
||||
logs
|
||||
} else {
|
||||
getCommitLog(i, logs :+ new CommitInfo(revCommit))
|
||||
}
|
||||
}
|
||||
case false => logs
|
||||
}
|
||||
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(to)))
|
||||
|
||||
val commits = getCommitLog(revWalk.iterator, Nil)
|
||||
revWalk.release
|
||||
|
||||
commits.reverse
|
||||
}
|
||||
|
||||
// TODO swap parameters 'from' and 'to'!?
|
||||
def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] =
|
||||
getCommitLogs(git, to)(_.getName == from)
|
||||
|
||||
/**
|
||||
* Returns the latest RevCommit of the specified path.
|
||||
@@ -317,51 +350,11 @@ object JGitUtil {
|
||||
* @return the list of latest commit
|
||||
*/
|
||||
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
|
||||
|
||||
val map = new scala.collection.mutable.HashMap[String, RevCommit]
|
||||
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
|
||||
//revWalk.sort(RevSort.REVERSE);
|
||||
val i = revWalk.iterator
|
||||
|
||||
while(i.hasNext && map.size != paths.length){
|
||||
val commit = i.next
|
||||
if(commit.getParentCount == 0){
|
||||
// Initial commit
|
||||
val treeWalk = new TreeWalk(git.getRepository)
|
||||
treeWalk.reset()
|
||||
treeWalk.setRecursive(true)
|
||||
treeWalk.addTree(commit.getTree)
|
||||
while (treeWalk.next) {
|
||||
paths.foreach { path =>
|
||||
if(treeWalk.getPathString.startsWith(path) && !map.contains(path)){
|
||||
map.put(path, commit)
|
||||
}
|
||||
}
|
||||
}
|
||||
treeWalk.release
|
||||
} else {
|
||||
(0 to commit.getParentCount - 1).foreach { i =>
|
||||
val parent = revWalk.parseCommit(commit.getParent(i).getId())
|
||||
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
|
||||
df.setRepository(git.getRepository)
|
||||
df.setDiffComparator(RawTextComparator.DEFAULT)
|
||||
df.setDetectRenames(true)
|
||||
val diffs = df.scan(parent.getTree(), commit.getTree)
|
||||
diffs.asScala.foreach { diff =>
|
||||
paths.foreach { path =>
|
||||
if(diff.getChangeType != ChangeType.DELETE && diff.getNewPath.startsWith(path) && !map.contains(path)){
|
||||
map.put(path, commit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revWalk.release
|
||||
}
|
||||
map.toMap
|
||||
val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||
paths.map { path =>
|
||||
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
|
||||
(path, commit)
|
||||
}.toMap
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,15 +380,18 @@ object JGitUtil {
|
||||
} catch {
|
||||
case e: MissingObjectException => None
|
||||
}
|
||||
|
||||
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): List[DiffInfo] = {
|
||||
|
||||
/**
|
||||
* Returns the tuple of diff of the given commit and the previous commit id.
|
||||
*/
|
||||
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
|
||||
@scala.annotation.tailrec
|
||||
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
|
||||
i.hasNext match {
|
||||
case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next)
|
||||
case _ => logs
|
||||
}
|
||||
|
||||
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
|
||||
|
||||
@@ -407,26 +403,8 @@ object JGitUtil {
|
||||
if(commits.length >= 2){
|
||||
// not initial commit
|
||||
val oldCommit = commits(1)
|
||||
|
||||
// get diff between specified commit and its previous commit
|
||||
val reader = git.getRepository.newObjectReader
|
||||
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(oldCommit.name + "^{tree}"))
|
||||
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(id + "^{tree}"))
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
|
||||
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
|
||||
} else {
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
||||
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")),
|
||||
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
|
||||
}
|
||||
}.toList
|
||||
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
|
||||
|
||||
} else {
|
||||
// initial commit
|
||||
val walk = new TreeWalk(git.getRepository)
|
||||
@@ -437,14 +415,35 @@ object JGitUtil {
|
||||
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, None)
|
||||
} else {
|
||||
DiffInfo(ChangeType.ADD, null, walk.getPathString, None,
|
||||
JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
|
||||
JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
}))
|
||||
}
|
||||
walk.release
|
||||
buffer.toList
|
||||
(buffer.toList, None)
|
||||
}
|
||||
}
|
||||
|
||||
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
|
||||
val reader = git.getRepository.newObjectReader
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
|
||||
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
|
||||
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
|
||||
} else {
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
||||
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
}
|
||||
}.toList
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the list of branch names of the specified commit.
|
||||
*/
|
||||
@@ -483,4 +482,41 @@ object JGitUtil {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
def initRepository(dir: java.io.File): Unit = {
|
||||
val repository = new RepositoryBuilder().setGitDir(dir).setBare.build
|
||||
try {
|
||||
repository.create
|
||||
setReceivePack(repository)
|
||||
} finally {
|
||||
repository.close
|
||||
}
|
||||
}
|
||||
|
||||
def cloneRepository(from: java.io.File, to: java.io.File): Unit = {
|
||||
val git = Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call
|
||||
try {
|
||||
setReceivePack(git.getRepository)
|
||||
} finally {
|
||||
git.getRepository.close
|
||||
}
|
||||
}
|
||||
|
||||
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
|
||||
|
||||
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
|
||||
val config = repository.getConfig
|
||||
config.setBoolean("http", null, "receivepack", true)
|
||||
config.save
|
||||
}
|
||||
|
||||
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
|
||||
revstr: String = ""): Option[(ObjectId, String)] = {
|
||||
Seq(
|
||||
if(revstr.isEmpty) repository.repository.defaultBranch else revstr,
|
||||
repository.branchList.head
|
||||
).map { rev =>
|
||||
(git.getRepository.resolve(rev), rev)
|
||||
}.find(_._1 != null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
107
src/main/scala/util/LDAPUtil.scala
Normal file
107
src/main/scala/util/LDAPUtil.scala
Normal file
@@ -0,0 +1,107 @@
|
||||
package util
|
||||
|
||||
import service.SystemSettingsService.Ldap
|
||||
import service.SystemSettingsService
|
||||
import com.novell.ldap._
|
||||
import service.SystemSettingsService.Ldap
|
||||
import scala.Some
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/**
|
||||
* Utility for LDAP authentication.
|
||||
*/
|
||||
object LDAPUtil {
|
||||
|
||||
private val LDAP_VERSION: Int = 3
|
||||
|
||||
/**
|
||||
* Try authentication by LDAP using given configuration.
|
||||
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage).
|
||||
*/
|
||||
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
|
||||
bind(
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
ldapSettings.bindDN.getOrElse(""),
|
||||
ldapSettings.bindPassword.getOrElse("")
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
|
||||
case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
|
||||
case None => Left("User does not exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
case None => Left("System LDAP authentication failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = {
|
||||
bind(
|
||||
ldapSettings.host,
|
||||
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
|
||||
userDN,
|
||||
password
|
||||
) match {
|
||||
case Some(conn) => {
|
||||
withConnection(conn) { conn =>
|
||||
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
|
||||
case Some(mailAddress) => Right(mailAddress)
|
||||
case None => Left("Can't find mail address.")
|
||||
}
|
||||
}
|
||||
}
|
||||
case None => Left("User LDAP Authentication Failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
|
||||
val conn: LDAPConnection = new LDAPConnection
|
||||
try {
|
||||
conn.connect(host, port)
|
||||
conn.bind(LDAP_VERSION, dn, password.getBytes)
|
||||
Some(conn)
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
if (conn.isConnected) conn.disconnect()
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
|
||||
try {
|
||||
f(conn)
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
|
||||
@tailrec
|
||||
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
|
||||
if(results.hasMore){
|
||||
getEntries(results, entries :+ (try {
|
||||
Option(results.next)
|
||||
} catch {
|
||||
case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD)
|
||||
}))
|
||||
} else {
|
||||
entries.flatten
|
||||
}
|
||||
}
|
||||
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)).collectFirst {
|
||||
case x => x.getDN
|
||||
}
|
||||
}
|
||||
|
||||
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = {
|
||||
val results = conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)
|
||||
if (results.hasMore) {
|
||||
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/main/scala/util/LockUtil.scala
Normal file
36
src/main/scala/util/LockUtil.scala
Normal file
@@ -0,0 +1,36 @@
|
||||
package util
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.locks.{ReentrantLock, Lock}
|
||||
|
||||
object LockUtil {
|
||||
|
||||
/**
|
||||
* lock objects
|
||||
*/
|
||||
private val locks = new ConcurrentHashMap[String, Lock]()
|
||||
|
||||
/**
|
||||
* Returns the lock object for the specified repository.
|
||||
*/
|
||||
private def getLockObject(key: String): Lock = synchronized {
|
||||
if(!locks.containsKey(key)){
|
||||
locks.put(key, new ReentrantLock())
|
||||
}
|
||||
locks.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes a given function which modifies the working copy of the wiki repository.
|
||||
*/
|
||||
def lock[T](key: String)(f: => T): T = {
|
||||
val lock = getLockObject(key)
|
||||
try {
|
||||
lock.lock()
|
||||
f
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
104
src/main/scala/util/Notifier.scala
Normal file
104
src/main/scala/util/Notifier.scala
Normal file
@@ -0,0 +1,104 @@
|
||||
package util
|
||||
|
||||
import scala.concurrent._
|
||||
import ExecutionContext.Implicits.global
|
||||
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import app.Context
|
||||
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
|
||||
import servlet.Database
|
||||
import SystemSettingsService.Smtp
|
||||
|
||||
trait Notifier extends RepositoryService with AccountService with IssuesService {
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||
(msg: String => String)(implicit context: Context): Unit
|
||||
|
||||
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
|
||||
(
|
||||
// individual repository's owner
|
||||
issue.userName ::
|
||||
// collaborators
|
||||
getCollaborators(issue.userName, issue.repositoryName) :::
|
||||
// participants
|
||||
issue.openedUserName ::
|
||||
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
|
||||
)
|
||||
.distinct
|
||||
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
|
||||
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
|
||||
|
||||
}
|
||||
|
||||
object Notifier {
|
||||
// TODO We want to be able to switch to mock.
|
||||
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
|
||||
case settings if settings.notification => new Mailer(settings.smtp.get)
|
||||
case _ => new MockMailer
|
||||
}
|
||||
|
||||
def msgIssue(url: String) = (content: String) => s"""
|
||||
|${content}<br/>
|
||||
|--<br/>
|
||||
|<a href="${url}">View it on GitBucket</a>
|
||||
""".stripMargin
|
||||
|
||||
def msgPullRequest(url: String) = (content: String) => s"""
|
||||
|${content}<hr/>
|
||||
|View, comment on, or merge it at:<br/>
|
||||
|<a href="${url}">${url}</a>
|
||||
""".stripMargin
|
||||
|
||||
def msgComment(url: String) = (content: String) => s"""
|
||||
|${content}<br/>
|
||||
|--<br/>
|
||||
|<a href="${url}">View it on GitBucket</a>
|
||||
""".stripMargin
|
||||
|
||||
def msgStatus(url: String) = (content: String) => s"""
|
||||
|${content} <a href="${url}">#${url split('/') last}</a>
|
||||
""".stripMargin
|
||||
}
|
||||
|
||||
class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
private val logger = LoggerFactory.getLogger(classOf[Mailer])
|
||||
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||
(msg: String => String)(implicit context: Context) = {
|
||||
val f = future {
|
||||
val email = new HtmlEmail
|
||||
email.setHostName(smtp.host)
|
||||
email.setSmtpPort(smtp.port.get)
|
||||
smtp.user.foreach { user =>
|
||||
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
|
||||
}
|
||||
smtp.ssl.foreach { ssl =>
|
||||
email.setSSLOnConnect(ssl)
|
||||
}
|
||||
email.setFrom("notifications@gitbucket.com", context.loginAccount.get.userName)
|
||||
email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true)))
|
||||
|
||||
// TODO Can we use the Database Session in other than Transaction Filter?
|
||||
Database(context.request.getServletContext) withSession {
|
||||
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
|
||||
email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})")
|
||||
recipients(issue) {
|
||||
email.getToAddresses.clear
|
||||
email.addTo(_).send
|
||||
}
|
||||
}
|
||||
}
|
||||
"Notifications Successful."
|
||||
}
|
||||
f onSuccess {
|
||||
case s => logger.debug(s)
|
||||
}
|
||||
f onFailure {
|
||||
case t => logger.error("Notifications Failed.", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
class MockMailer extends Notifier {
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||
(msg: String => String)(implicit context: Context): Unit = {}
|
||||
}
|
||||
@@ -1,11 +1,40 @@
|
||||
package util
|
||||
|
||||
import java.net.{URLDecoder, URLEncoder}
|
||||
import org.mozilla.universalchardet.UniversalDetector
|
||||
|
||||
object StringUtil {
|
||||
|
||||
def encrypt(value: String): String = {
|
||||
def sha1(value: String): String = {
|
||||
val md = java.security.MessageDigest.getInstance("SHA-1")
|
||||
md.update(value.getBytes)
|
||||
md.digest.map(b => "%02x".format(b)).mkString
|
||||
}
|
||||
|
||||
def md5(value: String): String = {
|
||||
val md = java.security.MessageDigest.getInstance("MD5")
|
||||
md.update(value.getBytes)
|
||||
md.digest.map(b => "%02x".format(b)).mkString
|
||||
}
|
||||
|
||||
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
|
||||
|
||||
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
||||
|
||||
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
|
||||
|
||||
def escapeHtml(value: String): String =
|
||||
value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
||||
|
||||
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
|
||||
|
||||
def detectEncoding(content: Array[Byte]): String = {
|
||||
val detector = new UniversalDetector(null)
|
||||
detector.handleData(content, 0, content.length)
|
||||
detector.dataEnd()
|
||||
detector.getDetectedCharset match {
|
||||
case null => "UTF-8"
|
||||
case e => e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package util
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import scala.Some
|
||||
|
||||
trait Validations {
|
||||
|
||||
@@ -9,11 +8,11 @@ trait Validations {
|
||||
* Constraint for the identifier such as user name, repository name or page name.
|
||||
*/
|
||||
def identifier: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
override def validate(name: String, value: String): Option[String] =
|
||||
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){
|
||||
Some("%s contains invalid character.".format(name))
|
||||
Some(s"${name} contains invalid character.")
|
||||
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||
Some("%s starts with invalid character.".format(name))
|
||||
Some(s"${name} starts with invalid character.")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
37
src/main/scala/view/AvatarImageProvider.scala
Normal file
37
src/main/scala/view/AvatarImageProvider.scala
Normal file
@@ -0,0 +1,37 @@
|
||||
package view
|
||||
|
||||
import service.RequestCache
|
||||
import twirl.api.Html
|
||||
import util.StringUtil
|
||||
|
||||
trait AvatarImageProvider { self: RequestCache =>
|
||||
|
||||
/**
|
||||
* Returns <img> which displays the avatar icon.
|
||||
* Looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
protected def getAvatarImageHtml(userName: String, size: Int,
|
||||
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
|
||||
|
||||
val src = getAccountByUserName(userName).map { account =>
|
||||
if(account.image.isEmpty && getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${userName}/_avatar"""
|
||||
}
|
||||
} getOrElse {
|
||||
if(mailAddress.nonEmpty && getSystemSettings().gravatar){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${userName}/_avatar"""
|
||||
}
|
||||
}
|
||||
|
||||
if(tooltip){
|
||||
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
|
||||
} else {
|
||||
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/scala/view/LinkConverter.scala
Normal file
33
src/main/scala/view/LinkConverter.scala
Normal file
@@ -0,0 +1,33 @@
|
||||
package view
|
||||
|
||||
import service.RequestCache
|
||||
import util.Implicits.RichString
|
||||
|
||||
trait LinkConverter { self: RequestCache =>
|
||||
|
||||
/**
|
||||
* Converts issue id, username and commit id to link.
|
||||
*/
|
||||
protected def convertRefsLinks(value: String, repository: service.RepositoryService.RepositoryInfo,
|
||||
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
|
||||
value
|
||||
// escape HTML tags
|
||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
||||
// convert issue id to link
|
||||
.replaceBy(("(^|\\W)" + issueIdPrefix + "(\\d+)(\\W|$)").r){ m =>
|
||||
if(getIssue(repository.owner, repository.name, m.group(2)).isDefined){
|
||||
Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
|
||||
} else {
|
||||
Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
|
||||
}
|
||||
}
|
||||
// convert @username to link
|
||||
.replaceBy("(^|\\W)@([a-zA-Z0-9\\-_]+)(\\W|$)".r){ m =>
|
||||
getAccountByUserName(m.group(2)).map { _ =>
|
||||
s"""${m.group(1)}<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>${m.group(3)}"""
|
||||
}
|
||||
}
|
||||
// convert commit id to link
|
||||
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", s"""$$1<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>$$3""")
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package view
|
||||
|
||||
import util.StringUtil
|
||||
import org.parboiled.common.StringUtils
|
||||
import org.pegdown._
|
||||
import org.pegdown.ast._
|
||||
import org.pegdown.LinkRenderer.Rendering
|
||||
import scala.collection.JavaConverters._
|
||||
import service.RequestCache
|
||||
|
||||
object Markdown {
|
||||
|
||||
@@ -12,12 +14,17 @@ object Markdown {
|
||||
* Converts Markdown of Wiki pages to HTML.
|
||||
*/
|
||||
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): String = {
|
||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
|
||||
// escape issue id
|
||||
val source = if(enableRefsLink){
|
||||
markdown.replaceAll("(^|\\W)#([0-9]+)(\\W|$)", "$1issue:$2$3")
|
||||
} else markdown
|
||||
|
||||
val rootNode = new PegDownProcessor(
|
||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES
|
||||
).parseMarkdown(markdown.toCharArray)
|
||||
).parseMarkdown(source.toCharArray)
|
||||
|
||||
new GitBucketHtmlSerializer(markdown, context, repository, enableWikiLink, enableCommitLink, enableIssueLink).toHtml(rootNode)
|
||||
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +40,10 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
|
||||
} else {
|
||||
(text, text)
|
||||
}
|
||||
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") +
|
||||
"/wiki/" + java.net.URLEncoder.encode(page.replace(' ', '-'), "UTF-8")
|
||||
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
|
||||
new Rendering(url, label)
|
||||
} catch {
|
||||
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException();
|
||||
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
|
||||
}
|
||||
} else {
|
||||
super.render(node)
|
||||
@@ -64,24 +70,13 @@ class GitBucketVerbatimSerializer extends VerbatimSerializer {
|
||||
|
||||
class GitBucketHtmlSerializer(
|
||||
markdown: String,
|
||||
context: app.Context,
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean,
|
||||
enableCommitLink: Boolean,
|
||||
enableIssueLink: Boolean
|
||||
) extends ToHtmlSerializer(
|
||||
enableRefsLink: Boolean
|
||||
)(implicit val context: app.Context) extends ToHtmlSerializer(
|
||||
new GitBucketLinkRender(context, repository, enableWikiLink),
|
||||
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
|
||||
) {
|
||||
|
||||
override def toHtml(rootNode: RootNode): String = {
|
||||
val html = super.toHtml(rootNode)
|
||||
if(enableIssueLink){
|
||||
// convert marked issue id to link.
|
||||
html.replaceAll("#\\{\\{\\{\\{(\\d+)\\}\\}\\}\\}",
|
||||
"<a href=\"%s/%s/%s/issues/$1\">#$1</a>".format(context.path, repository.owner, repository.name))
|
||||
} else html
|
||||
}
|
||||
) with LinkConverter with RequestCache {
|
||||
|
||||
override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
|
||||
printer.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/>")
|
||||
@@ -108,30 +103,13 @@ class GitBucketHtmlSerializer(
|
||||
}
|
||||
|
||||
override def visit(node: TextNode) {
|
||||
// convert commit id to link.
|
||||
val text1 = if(enableCommitLink) node.getText.replaceAll("(^|\\W)([0-9a-f]{40})(\\W|$)",
|
||||
"<a href=\"%s/%s/%s/commit/$2\">$2</a>".format(context.path, repository.owner, repository.name))
|
||||
else node.getText
|
||||
|
||||
// mark issue id to link
|
||||
val startIndex = node.getStartIndex
|
||||
val text2 = if(enableIssueLink && startIndex > 0 && markdown.charAt(startIndex - 1) == '#'){
|
||||
text1.replaceFirst("^(\\d+)(\\W|$)", "{{{{$1}}}}")
|
||||
} else text1
|
||||
// convert commit id and username to link.
|
||||
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
|
||||
|
||||
if (abbreviations.isEmpty) {
|
||||
printer.print(text2)
|
||||
printer.print(text)
|
||||
} else {
|
||||
printWithAbbreviations(text2)
|
||||
}
|
||||
}
|
||||
|
||||
override def visit(node: HeaderNode) {
|
||||
if(enableIssueLink && markdown.substring(node.getStartIndex, node.getEndIndex - 1).startsWith("#")){
|
||||
printer.print("#" * node.getLevel)
|
||||
visitChildren(node)
|
||||
} else {
|
||||
printTag(node, "h" + node.getLevel)
|
||||
printWithAbbreviations(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ package view
|
||||
import java.util.Date
|
||||
import java.text.SimpleDateFormat
|
||||
import twirl.api.Html
|
||||
import util.StringUtil
|
||||
import service.RequestCache
|
||||
|
||||
/**
|
||||
* Provides helper methods for Twirl templates.
|
||||
*/
|
||||
object helpers {
|
||||
object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
|
||||
|
||||
/**
|
||||
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
|
||||
@@ -29,39 +31,71 @@ object helpers {
|
||||
* Converts Markdown of Wiki pages to HTML.
|
||||
*/
|
||||
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): Html = {
|
||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink))
|
||||
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
|
||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <img> which displays the avatar icon.
|
||||
* Looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(userName, size, "", tooltip)
|
||||
|
||||
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
|
||||
|
||||
/**
|
||||
* Converts commit id, issue id and username to the link.
|
||||
*/
|
||||
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
|
||||
Html(convertRefsLinks(value, repository))
|
||||
|
||||
def cut(value: String, length: Int): String =
|
||||
if(value.length > length){
|
||||
value.substring(0, length) + "..."
|
||||
} else {
|
||||
value
|
||||
}
|
||||
|
||||
def activityMessage(message: String)(implicit context: app.Context): Html =
|
||||
Html(message
|
||||
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
|
||||
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
|
||||
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
|
||||
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
|
||||
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
|
||||
.replaceAll("\\[user:([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1">$$1</a>""")
|
||||
)
|
||||
|
||||
def urlEncode(value: String): String = StringUtil.urlEncode(value)
|
||||
|
||||
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
|
||||
|
||||
/**
|
||||
* Generates the url to the repository.
|
||||
*/
|
||||
def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String =
|
||||
"%s/%s/%s".format(context.path, repository.owner, repository.name)
|
||||
s"${context.path}/${repository.owner}/${repository.name}"
|
||||
|
||||
/**
|
||||
* Generates the url to the account page.
|
||||
*/
|
||||
def url(userName: String)(implicit context: app.Context): String = "%s/%s".format(context.path, userName)
|
||||
def url(userName: String)(implicit context: app.Context): String =
|
||||
s"${context.path}/${userName}"
|
||||
|
||||
/**
|
||||
* Returns the url to the root of assets.
|
||||
*/
|
||||
def assets(implicit context: app.Context): String = "%s/assets".format(context.path)
|
||||
def assets(implicit context: app.Context): String =
|
||||
s"${context.path}/assets"
|
||||
|
||||
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
|
||||
|
||||
/**
|
||||
* Converts issue id and commit id to link.
|
||||
* Implicit conversion to add mkHtml() to Seq[Html].
|
||||
*/
|
||||
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
|
||||
Html(value
|
||||
// escape HTML tags
|
||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
||||
// convert issue id to link
|
||||
.replaceAll("(^|\\W)#(\\d+)(\\W|$)", "$1<a href=\"%s/%s/%s/issues/$2\">#$2</a>$3".format(context.path, repository.owner, repository.name))
|
||||
// convert commit id to link
|
||||
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", "$1<a href=\"%s/%s/%s/commit/$2\">$2</a>$3").format(context.path, repository.owner, repository.name))
|
||||
|
||||
implicit def extendsHtmlSeq(seq: Seq[Html]) = new {
|
||||
implicit class RichHtmlSeq(seq: Seq[Html]) {
|
||||
def mkHtml(separator: String) = Html(seq.mkString(separator))
|
||||
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))
|
||||
}
|
||||
|
||||
6
src/main/twirl/account/activity.scala.html
Normal file
6
src/main/twirl/account/activity.scala.html
Normal file
@@ -0,0 +1,6 @@
|
||||
@(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main(account, groupNames, "activity"){
|
||||
@helper.html.activities(activities)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@(account: Option[model.Account])(implicit context: app.Context)
|
||||
@(account: Option[model.Account], info: Option[Any])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main((if(account.isDefined) "Edit your profile" else "Create your account")){
|
||||
@@ -7,34 +7,47 @@
|
||||
} else {
|
||||
<h3>Create your account</h3>
|
||||
}
|
||||
@helper.html.information(info)
|
||||
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true">
|
||||
@if(account.isEmpty){
|
||||
<fieldset>
|
||||
<label for="userName"><strong>User name</strong></label>
|
||||
<input type="text" name="userName" id="userName" value=""/>
|
||||
<span id="error-userName" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="password"><strong>Password</strong>
|
||||
@if(account.nonEmpty){
|
||||
(Input to change password)
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
@if(account.isEmpty){
|
||||
<fieldset>
|
||||
<label for="userName"><strong>User name</strong></label>
|
||||
<input type="text" name="userName" id="userName" value=""/>
|
||||
<span id="error-userName" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value=""/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="url"><strong>URL (Optional)</strong></label>
|
||||
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
<fieldset>
|
||||
<label for="password"><strong>Password</strong>
|
||||
@if(account.nonEmpty){
|
||||
(Input to change password)
|
||||
}
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value=""/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="url"><strong>URL (Optional)</strong></label>
|
||||
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="avatar"><strong>Image (Optional)</strong></label>
|
||||
@helper.html.uploadavatar(account)
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="margin">
|
||||
@if(account.isDefined){
|
||||
<input type="submit" class="btn btn-success" value="Save"/>
|
||||
<a href="@url(account.get.userName)" class="btn">Cancel</a>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
@(account: model.Account, repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(account.userName){
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<div class="block">
|
||||
<div class="block-header">@account.userName</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span8">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#">Repositories</a></li>
|
||||
<!--
|
||||
<li><a href="#">Activity</a></li>
|
||||
-->
|
||||
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
||||
<li class="pull-right">
|
||||
<div class="button-group">
|
||||
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@repositories.map { repository =>
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
<a href="@url(repository.owner)">@repository.owner</a>
|
||||
/
|
||||
<a href="@url(repository)">@repository.name</a>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
</div>
|
||||
@if(repository.repository.description.isDefined){
|
||||
<div>@repository.repository.description</div>
|
||||
}
|
||||
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
48
src/main/twirl/account/main.scala.html
Normal file
48
src/main/twirl/account/main.scala.html
Normal file
@@ -0,0 +1,48 @@
|
||||
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(account.userName){
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<div class="block">
|
||||
<div class="account-image">@avatar(account.userName, 200)</div>
|
||||
<div class="block-header">@account.userName</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
@if(account.url.isDefined){
|
||||
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||
}
|
||||
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
||||
</div>
|
||||
@if(groupNames.nonEmpty){
|
||||
<div>
|
||||
<div>Groups</div>
|
||||
@groupNames.map { groupName =>
|
||||
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="span8">
|
||||
<ul class="nav nav-tabs">
|
||||
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
|
||||
@if(account.isGroupAccount){
|
||||
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
|
||||
} else {
|
||||
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
|
||||
}
|
||||
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
||||
<li class="pull-right">
|
||||
<div class="button-group">
|
||||
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@body
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
16
src/main/twirl/account/members.scala.html
Normal file
16
src/main/twirl/account/members.scala.html
Normal file
@@ -0,0 +1,16 @@
|
||||
@(account: model.Account, members: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main(account, Nil, "members"){
|
||||
@if(members.isEmpty){
|
||||
No members
|
||||
} else {
|
||||
@members.map { userName =>
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
@avatar(userName, 20) <a href="@url(userName)">@userName</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/twirl/account/repositories.scala.html
Normal file
26
src/main/twirl/account/repositories.scala.html
Normal file
@@ -0,0 +1,26 @@
|
||||
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main(account, groupNames, "repositories"){
|
||||
@if(repositories.isEmpty){
|
||||
No repositories
|
||||
} else {
|
||||
@repositories.map { repository =>
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
<a href="@url(repository)">@repository.name</a>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
</div>
|
||||
@if(repository.repository.originUserName.isDefined){
|
||||
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
|
||||
}
|
||||
@if(repository.repository.description.isDefined){
|
||||
<div>@repository.repository.description</div>
|
||||
}
|
||||
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
@(settings: service.SystemSettingsService.SystemSettings)(implicit context: app.Context)
|
||||
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("System Settings"){
|
||||
@menu("system"){
|
||||
@helper.html.information(info)
|
||||
<form action="@path/admin/system" method="POST" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">System Settings</div>
|
||||
<div class="box-content">
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
<!--====================================================================-->
|
||||
<label><strong>Account registration</strong></label>
|
||||
<fieldset>
|
||||
<label>
|
||||
@@ -18,6 +22,125 @@
|
||||
<strong>Deny</strong> - Only administrators can create account.
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Services -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><strong>Services</strong></label>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
|
||||
Gravatar
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Authentication -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><strong>Authentication</strong></label>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
|
||||
LDAP
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="form-horizontal ldap">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapHost">LDAP Host</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapHost" name="ldap.host" value="@settings.ldap.map(_.host)"/>
|
||||
<span id="error-ldap_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapPort">LDAP Port</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapPort" name="ldap.port" class="input-mini" value="@settings.ldap.map(_.port)"/>
|
||||
<span id="error-ldap_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBindDN">Bind DN</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapBindDN" name="ldap.bindDN" value="@settings.ldap.map(_.bindDN)"/>
|
||||
<span id="error-ldap_bindDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBindPassword">Bind Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" value="@settings.ldap.map(_.bindPassword)"/>
|
||||
<span id="error-ldap_bindPassword" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapBaseDN">Base DN</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapBaseDN" name="ldap.baseDN" value="@settings.ldap.map(_.baseDN)"/>
|
||||
<span id="error-ldap_baseDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapUserNameAttribute">User name attribute</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" value="@settings.ldap.map(_.userNameAttribute)"/>
|
||||
<span id="error-ldap_userNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" value="@settings.ldap.map(_.mailAttribute)"/>
|
||||
<span id="error-ldap_mailAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Notification email -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><strong>Notification email</strong></label>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
|
||||
Send notifications
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="form-horizontal notification">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="smtpHost">SMTP Host</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="smtpHost" name="smtp.host" value="@settings.smtp.map(_.host)"/>
|
||||
<span id="error-smtp_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="smtpPort">SMTP Port</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="smtpPort" name="smtp.port" class="input-mini" value="@settings.smtp.map(_.port)"/>
|
||||
<span id="error-smtp_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="smtpUser">SMTP User</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="smtpUser" name="smtp.user" value="@settings.smtp.map(_.user)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="smtpPassword">SMTP Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" id="smtpPassword" name="smtp.password" value="@settings.smtp.map(_.password)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset>
|
||||
@@ -25,4 +148,15 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#notification').change(function(){
|
||||
$('.notification input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
$('#ldapAuthentication').change(function(){
|
||||
$('.ldap input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
@@ -1,45 +0,0 @@
|
||||
@(account: Option[model.Account])(implicit context: app.Context)
|
||||
@import context._
|
||||
@html.main(if(account.isEmpty) "New User" else "Update User"){
|
||||
@admin.html.menu("users"){
|
||||
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_new} else {@path/admin/users/@account.get.userName/_edit}" validate="true">
|
||||
<fieldset>
|
||||
<label for="userName"><strong>Username</strong></label>
|
||||
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
<span id="error-userName" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="password"><strong>Password</strong>
|
||||
@if(account.isDefined){
|
||||
(Input to change password)
|
||||
}
|
||||
</label>
|
||||
<input type="password" name="password" id="password" value="" autocomplete="off"/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label><strong>User Type</strong></label>
|
||||
<label for="userType_Normal">
|
||||
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
|
||||
</label>
|
||||
<label for="userType_Admin">
|
||||
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label><strong>URL (Optional)</strong></label>
|
||||
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
|
||||
<a href="@path/admin/users" class="btn">Cancel</a>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
121
src/main/twirl/admin/users/group.scala.html
Normal file
121
src/main/twirl/admin/users/group.scala.html
Normal file
@@ -0,0 +1,121 @@
|
||||
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
|
||||
@admin.html.menu("users"){
|
||||
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span7">
|
||||
<fieldset>
|
||||
<label for="groupName"><strong>Group name</strong></label>
|
||||
<span id="error-groupName" class="error"></span>
|
||||
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label><strong>URL (Optional)</strong></label>
|
||||
<span id="error-url" class="error"></span>
|
||||
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="avatar"><strong>Image (Optional)</strong></label>
|
||||
@helper.html.uploadavatar(account)
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span5">
|
||||
<fieldset>
|
||||
<label><strong>Members</strong></label>
|
||||
<ul id="members" class="collaborator">
|
||||
@members.map { userName =>
|
||||
<li data-name="@userName">
|
||||
<a href="@path/@url(userName)">@userName</a>
|
||||
<a href="#" class="remove">(remove)</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<input type="text" id="memberName" style="width: 200px; margin-bottom: 0px;"/>
|
||||
<input type="button" class="btn" value="Add" id="addMember"/>
|
||||
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
|
||||
<div>
|
||||
<span class="error" id="error-memberName"></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="margin">
|
||||
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
|
||||
<a href="@path/admin/users" class="btn">Cancel</a>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#memberName').typeahead({
|
||||
source: function (query, process) {
|
||||
return $.get('@path/_user/proposals', { query: query },
|
||||
function (data) {
|
||||
return process(data.options);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#addMember').click(function(){
|
||||
$('#error-memberName').text('');
|
||||
var userName = $('#memberName').val();
|
||||
|
||||
// check empty
|
||||
if($.trim(userName) == ''){
|
||||
return false;
|
||||
}
|
||||
|
||||
// check duplication
|
||||
var exists = $('#members li').filter(function(){
|
||||
return $(this).data('name') == userName;
|
||||
}).length > 0;
|
||||
if(exists){
|
||||
$('#error-memberName').text('User has been already added.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// check existence
|
||||
$.post('@path/admin/users/_usercheck', {
|
||||
'userName': userName
|
||||
}, function(data, status){
|
||||
if(data == 'true'){
|
||||
// add member
|
||||
$('#members').append($('<li>')
|
||||
.data('name', userName)
|
||||
.append($('<a>').attr('href', '@path/' + userName).text(userName))
|
||||
.append(' ')
|
||||
.append($('<a>').attr('href', '#').addClass('remove').text('(remove)')));
|
||||
$('#memberName').val('');
|
||||
|
||||
// update hidden value
|
||||
var userNames = $('#members li').map(function(i, e){
|
||||
return $(e).data('name');
|
||||
}).get().join(',');
|
||||
$('#memberNames').val(userNames);
|
||||
} else {
|
||||
$('#error-memberName').text('User does not exist.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove', function(){
|
||||
// remove member
|
||||
$(this).parent().remove();
|
||||
|
||||
// update hidden value
|
||||
var userNames = $('#members li').map(function(i, e){
|
||||
return $(e).data('name');
|
||||
}).get().join(',');
|
||||
$('#memberNames').val(userNames);
|
||||
});
|
||||
|
||||
// Don't submit form by ENTER key
|
||||
$('#memberName').keypress(function(e){
|
||||
console.log(e.keyCode);
|
||||
return !(e.keyCode == 13);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,29 +1,46 @@
|
||||
@(users: List[model.Account])(implicit context: app.Context)
|
||||
@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Manage Users"){
|
||||
@admin.html.menu("users"){
|
||||
<div style="text-align: right; margin-bottom: 4px;">
|
||||
<a href="@path/admin/users/_new" class="btn">New User</a>
|
||||
<a href="@path/admin/users/_newuser" class="btn">New User</a>
|
||||
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
|
||||
</div>
|
||||
<table class="table table-bordered table-hover">
|
||||
@users.map { account =>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
<a href="@path/admin/users/@account.userName/_edit">Edit</a>
|
||||
@if(account.isGroupAccount){
|
||||
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
|
||||
} else {
|
||||
<a href="@path/admin/users/@account.userName/_edituser">Edit</a>
|
||||
}
|
||||
</div>
|
||||
<div class="strong">
|
||||
@avatar(account.userName, 20)
|
||||
<a href="@url(account.userName)">@account.userName</a>
|
||||
@if(account.isAdmin){
|
||||
(Administrator)
|
||||
@if(account.isGroupAccount){
|
||||
(Group)
|
||||
} else {
|
||||
(Normal)
|
||||
@if(account.isAdmin){
|
||||
(Administrator)
|
||||
} else {
|
||||
(Normal)
|
||||
}
|
||||
}
|
||||
@if(account.isGroupAccount){
|
||||
@members(account.userName).map { userName =>
|
||||
@avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<hr>
|
||||
<i class="icon-envelope"></i> @account.mailAddress
|
||||
@if(!account.isGroupAccount){
|
||||
<i class="icon-envelope"></i> @account.mailAddress
|
||||
}
|
||||
@account.url.map { url =>
|
||||
<i class="icon-home"></i> @url
|
||||
}
|
||||
@@ -31,7 +48,9 @@
|
||||
<div>
|
||||
<span class="muted">Registered:</span> @datetime(account.registeredDate)
|
||||
<span class="muted">Updated:</span> @datetime(account.updatedDate)
|
||||
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
|
||||
@if(!account.isGroupAccount){
|
||||
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
58
src/main/twirl/admin/users/user.scala.html
Normal file
58
src/main/twirl/admin/users/user.scala.html
Normal file
@@ -0,0 +1,58 @@
|
||||
@(account: Option[model.Account])(implicit context: app.Context)
|
||||
@import context._
|
||||
@html.main(if(account.isEmpty) "New User" else "Update User"){
|
||||
@admin.html.menu("users"){
|
||||
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="userName"><strong>Username</strong></label>
|
||||
<span id="error-userName" class="error"></span>
|
||||
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
|
||||
</fieldset>
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
<fieldset>
|
||||
<label for="password">
|
||||
<strong>Password</strong>
|
||||
@if(account.isDefined){
|
||||
(Input to change password)
|
||||
}
|
||||
</label>
|
||||
<span id="error-password" class="error"></span>
|
||||
<input type="password" name="password" id="password" value="" autocomplete="off"/>
|
||||
</fieldset>
|
||||
}
|
||||
<fieldset>
|
||||
<label for="mailAddress"><strong>Mail Address</strong></label>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label><strong>User Type</strong></label>
|
||||
<label for="userType_Normal">
|
||||
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
|
||||
</label>
|
||||
<label for="userType_Admin">
|
||||
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label><strong>URL (Optional)</strong></label>
|
||||
<span id="error-url" class="error"></span>
|
||||
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="avatar"><strong>Image (Optional)</strong></label>
|
||||
@helper.html.uploadavatar(account)
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="margin">
|
||||
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
|
||||
<a href="@path/admin/users" class="btn">Cancel</a>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
48
src/main/twirl/dashboard/issues.scala.html
Normal file
48
src/main/twirl/dashboard/issues.scala.html
Normal file
@@ -0,0 +1,48 @@
|
||||
@(listparts: twirl.api.Html,
|
||||
allCount: Int,
|
||||
assignedCount: Int,
|
||||
createdByCount: Int,
|
||||
repositories: List[(String, String, Int)],
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
filter: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Your Issues"){
|
||||
@dashboard.html.tab("issues")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "all"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/repos@condition.toURL">
|
||||
<span class="count-right">@allCount</span>
|
||||
In your repositories
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "assigned"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/assigned@condition.toURL">
|
||||
<span class="count-right">@assignedCount</span>
|
||||
Assigned to you
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/created_by@condition.toURL">
|
||||
<span class="count-right">@createdByCount</span>
|
||||
Created by you
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@listparts
|
||||
</div>
|
||||
}
|
||||
40
src/main/twirl/dashboard/pulls.scala.html
Normal file
40
src/main/twirl/dashboard/pulls.scala.html
Normal file
@@ -0,0 +1,40 @@
|
||||
@(listparts: twirl.api.Html,
|
||||
counts: List[service.PullRequestService.PullRequestCount],
|
||||
repositories: List[(String, String, Int)],
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
filter: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Your Issues"){
|
||||
@dashboard.html.tab("pulls")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/owned@condition.toURL">
|
||||
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
|
||||
Yours
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "not_created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/public@condition.toURL">
|
||||
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
|
||||
Public
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/for/@owner/@name">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@listparts
|
||||
</div>
|
||||
}
|
||||
9
src/main/twirl/dashboard/tab.scala.html
Normal file
9
src/main/twirl/dashboard/tab.scala.html
Normal file
@@ -0,0 +1,9 @@
|
||||
@(active: String = "")(implicit context: app.Context)
|
||||
@import context._
|
||||
<ul class="nav nav-tabs">
|
||||
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
|
||||
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
|
||||
}
|
||||
</ul>
|
||||
@@ -1,11 +1,24 @@
|
||||
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<div class="pull-right">
|
||||
<div class="input-prepend">
|
||||
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
|
||||
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="head">
|
||||
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)">@repository.name</a>
|
||||
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
@defining(repository.repository){ x =>
|
||||
@if(repository.repository.originRepositoryName.isDefined){
|
||||
<div class="forked">
|
||||
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<table class="global-nav box-header">
|
||||
<tr>
|
||||
@@ -14,10 +27,22 @@
|
||||
</th>
|
||||
<th class="box-header@if(active=="issues"){ active}">
|
||||
<a href="@url(repository)/issues">Issues</a>
|
||||
@if(repository.issueCount > 0){
|
||||
<span class="badge">@repository.issueCount</span>
|
||||
}
|
||||
</th>
|
||||
<th class="box-header@if(active=="pulls"){ active}">
|
||||
<a href="@url(repository)/pulls">Pull Requests</a>
|
||||
@if(repository.pullCount > 0){
|
||||
<span class="badge">@repository.pullCount</span>
|
||||
}
|
||||
</th>
|
||||
<th class="box-header@if(active=="wiki"){ active}">
|
||||
<a href="@url(repository)/wiki">Wiki</a>
|
||||
</th>
|
||||
<th class="box-header@if(active=="network"){ active}">
|
||||
<a href="@url(repository)/network/members">Network</a>
|
||||
</th>
|
||||
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){
|
||||
<th class="box-header@if(active=="settings"){ active}">
|
||||
<a href="@url(repository)/settings">Settings</a>
|
||||
@@ -25,6 +50,8 @@
|
||||
}
|
||||
</tr>
|
||||
</table>
|
||||
<form method="POST" id="repository_form">
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
$('table.global-nav th.box-header').click(function(){
|
||||
|
||||
94
src/main/twirl/helper/activities.scala.html
Normal file
94
src/main/twirl/helper/activities.scala.html
Normal file
@@ -0,0 +1,94 @@
|
||||
@(activities: List[model.Activity])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
|
||||
@if(activities.isEmpty){
|
||||
No activity
|
||||
} else {
|
||||
@activities.map { activity =>
|
||||
<div class="block">
|
||||
@(activity.activityType match {
|
||||
case "open_issue" => detailActivity(activity, "activity-issue.png")
|
||||
case "comment_issue" => detailActivity(activity, "activity-comment.png")
|
||||
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
|
||||
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
|
||||
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
|
||||
case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
|
||||
case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
|
||||
case "create_branch" => simpleActivity(activity, "activity-branch.png")
|
||||
case "create_tag" => simpleActivity(activity, "activity-tag.png")
|
||||
case "fork" => simpleActivity(activity, "activity-fork.png")
|
||||
case "push" => customActivity(activity, "activity-commit.png"){
|
||||
<div class="small activity-message">
|
||||
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
|
||||
if(i == 3){
|
||||
<div>...</div>
|
||||
} else {
|
||||
<div>
|
||||
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
|
||||
<span>{commit.substring(41)}</span>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
case "create_wiki" => customActivity(activity, "activity-wiki.png"){
|
||||
<div class="small activity-message">
|
||||
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
|
||||
</div>
|
||||
}
|
||||
case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
|
||||
activity.additionalInfo.get.split(":") match {
|
||||
case Array(pageName, commitId) =>
|
||||
<div class="small activity-message">
|
||||
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
|
||||
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a>
|
||||
</div>
|
||||
case Array(pageName) =>
|
||||
<div class="small activity-message">
|
||||
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
})
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@detailActivity(activity: model.Activity, image: String) = {
|
||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
||||
<div class="activity-content">
|
||||
<div class="muted small">@datetime(activity.activityDate)</div>
|
||||
<div class="strong">
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
</div>
|
||||
@activity.additionalInfo.map { additionalInfo =>
|
||||
<div class=" activity-message">@additionalInfo</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
|
||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
||||
<div class="activity-content">
|
||||
<div class="muted small">@datetime(activity.activityDate)</div>
|
||||
<div class="strong">
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
</div>
|
||||
@additionalInfo
|
||||
</div>
|
||||
}
|
||||
|
||||
@simpleActivity(activity: model.Activity, image: String) = {
|
||||
<div class="activity-icon-small"><img src="@assets/common/images/@image"/></div>
|
||||
<div class="activity-content">
|
||||
<div>
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
<span class="muted small">@datetime(activity.activityDate)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
36
src/main/twirl/helper/copy.scala.html
Normal file
36
src/main/twirl/helper/copy.scala.html
Normal file
@@ -0,0 +1,36 @@
|
||||
@(id: String, value: String)(html: Html)
|
||||
<div class="input-append">
|
||||
@html
|
||||
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
|
||||
</div>
|
||||
<script>
|
||||
// copy to clipboard
|
||||
(function() {
|
||||
// Find ZeroClipboard.swf file URI from ZeroClipboard JavaScript file path.
|
||||
// NOTE(tanacasino) I think this way is wrong... but i don't know correct way.
|
||||
var moviePath = (function() {
|
||||
var zclipjs = "ZeroClipboard.min.js";
|
||||
var scripts = document.getElementsByTagName("script");
|
||||
var i = scripts.length;
|
||||
while(i--) {
|
||||
var match = scripts[i].src.match(zclipjs + "$");
|
||||
if(match) {
|
||||
return match.input.substr(0, match.input.length - 6) + 'swf';
|
||||
}
|
||||
}
|
||||
})();
|
||||
var clip = new ZeroClipboard($("#@id"), {
|
||||
moviePath: moviePath
|
||||
});
|
||||
var title = $('#@id').attr('title');
|
||||
$('#@id').removeAttr('title')
|
||||
clip.on('complete', function(client, args) {
|
||||
$(clip.htmlBridge).attr('title', 'copied!').tooltip('fixTitle').tooltip('show');
|
||||
$(clip.htmlBridge).attr('title', title).tooltip('fixTitle');
|
||||
});
|
||||
$(clip.htmlBridge).tooltip({
|
||||
title: title,
|
||||
placement: $('#@id').attr('data-placement')
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -1,4 +1,7 @@
|
||||
@(diffs: Seq[util.JGitUtil.DiffInfo], repository: service.RepositoryService.RepositoryInfo, commitId: Option[String])(implicit context: app.Context)
|
||||
@(diffs: Seq[util.JGitUtil.DiffInfo],
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
newCommitId: Option[String],
|
||||
oldCommitId: Option[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||
@@ -9,17 +12,27 @@
|
||||
<th style="font-weight: normal;" class="box-header">
|
||||
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
||||
@diff.oldPath -> @diff.newPath
|
||||
@if(newCommitId.isDefined){
|
||||
<div class="pull-right align-right">
|
||||
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
||||
@diff.newPath
|
||||
@if(newCommitId.isDefined){
|
||||
<div class="pull-right align-right">
|
||||
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if(diff.changeType == ChangeType.DELETE){
|
||||
@diff.oldPath
|
||||
}
|
||||
@if(commitId.isDefined){
|
||||
<div class="pull-right align-right">
|
||||
<a href="@url(repository)/blob/@commitId.get/@diff.newPath" class="btn btn-small">View file @@ @commitId.get.substring(0, 10)</a>
|
||||
</div>
|
||||
@if(oldCommitId.isDefined){
|
||||
<div class="pull-right align-right">
|
||||
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
@(body: Html)
|
||||
@(buttonValue: String = "", prefix: String = "")(body: Html)
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="icon-cog"></i>
|
||||
@if(buttonValue.isEmpty){
|
||||
<i class="icon-cog"></i>
|
||||
} else {
|
||||
@if(prefix.nonEmpty){
|
||||
<span class="muted">@prefix:</span>
|
||||
}
|
||||
<strong>@buttonValue</strong>
|
||||
}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
|
||||
7
src/main/twirl/helper/information.scala.html
Normal file
7
src/main/twirl/helper/information.scala.html
Normal file
@@ -0,0 +1,7 @@
|
||||
@(info: Option[Any])
|
||||
@if(info.isDefined){
|
||||
<div class="alert alert-info">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@info
|
||||
</div>
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String)
|
||||
@defining(view.Pagination(page, count, service.IssuesService.IssueLimit, width)){ p =>
|
||||
@defining(view.Pagination(page, count, limit, width)){ p =>
|
||||
@if(p.count > p.limit){
|
||||
<div class="pagination">
|
||||
<ul>
|
||||
@if(page == 1){
|
||||
<li class="disabled"><span>◀</span></li>
|
||||
<li class="disabled"><span>◀</span></li>
|
||||
} else {
|
||||
<li><a href="@baseURL&page=@(page - 1)">◀</a></li>
|
||||
<li><a href="@baseURL&page=@(page - 1)">◀</a></li>
|
||||
}
|
||||
@for(i <- 1 to p.max){
|
||||
@if(i == p.max && p.omitRight){
|
||||
<li><span>…</span></li>
|
||||
}
|
||||
@if(i == page){
|
||||
<li class="active"><span>@i</span></li>
|
||||
} else {
|
||||
@if(p.visibleFor(i)){
|
||||
<li><a href="@baseURL&page=@i">@i</a></li>
|
||||
}
|
||||
}
|
||||
@if(i == 1 && p.omitLeft){
|
||||
<li><span>…</span></li>
|
||||
}
|
||||
@if(i == p.max && p.omitRight){
|
||||
<li><span>…</span></li>
|
||||
}
|
||||
@if(i == page){
|
||||
<li class="active"><span>@i</span></li>
|
||||
} else {
|
||||
@if(p.visibleFor(i)){
|
||||
<li><a href="@baseURL&page=@i">@i</a></li>
|
||||
}
|
||||
}
|
||||
@if(i == 1 && p.omitLeft){
|
||||
<li><span>…</span></li>
|
||||
}
|
||||
}
|
||||
@if(page == p.max){
|
||||
<li class="disabled"><span>▶</span></li>
|
||||
<li class="disabled"><span>▶</span></li>
|
||||
} else {
|
||||
<li><a href="@baseURL&page=@(page + 1)">▶</a></li>
|
||||
<li><a href="@baseURL&page=@(page + 1)">▶</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean,
|
||||
enableCommitLink: Boolean, enableIssueLink: Boolean, style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context)
|
||||
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
|
||||
style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<div class="tabbable">
|
||||
@@ -30,10 +30,9 @@ $(function(){
|
||||
$('#preview').click(function(){
|
||||
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
|
||||
$.post('@url(repository)/_preview', {
|
||||
content : $('#content').val(),
|
||||
enableWikiLink : @enableWikiLink,
|
||||
enableCommitLink : @enableCommitLink,
|
||||
enableIssueLink : @enableIssueLink
|
||||
content : $('#content').val(),
|
||||
enableWikiLink : @enableWikiLink,
|
||||
enableRefsLink : @enableRefsLink
|
||||
}, function(data){
|
||||
$('#preview-area').html(data);
|
||||
prettyPrint();
|
||||
|
||||
52
src/main/twirl/helper/uploadavatar.scala.html
Normal file
52
src/main/twirl/helper/uploadavatar.scala.html
Normal file
@@ -0,0 +1,52 @@
|
||||
@(account: Option[model.Account])(implicit context: app.Context)
|
||||
@import context._
|
||||
<div id="avatar" class="muted">
|
||||
@if(account.nonEmpty && account.get.image.nonEmpty){
|
||||
<img src="@path/@account.get.userName/_avatar" style="with: 120px; height: 120px;"/>
|
||||
} else {
|
||||
<div id="clickable">
|
||||
<a href="https://www.gravatar.com/" target="_blank">Gravatar</a> is used
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if(account.nonEmpty && account.get.image.nonEmpty){
|
||||
<label>
|
||||
<input type="checkbox" name="clearImage"/> Clear image
|
||||
</label>
|
||||
}
|
||||
<input type="hidden" name="fileId" value=""/>
|
||||
<script>
|
||||
$(function(){
|
||||
var dropzone = new Dropzone('div#clickable', {
|
||||
url: '@path/upload/image',
|
||||
previewsContainer: 'div#avatar',
|
||||
paramName: 'file',
|
||||
parallelUploads: 1,
|
||||
thumbnailWidth: 120,
|
||||
thumbnailHeight: 120
|
||||
});
|
||||
|
||||
dropzone.on("success", function(file, id){
|
||||
$('div#clickable').remove();
|
||||
$('input[name=fileId]').val(id);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
div.dz-filename, div.dz-size, div.dz-progress, div.dz-success-mark, div.dz-error-mark, div.dz-error-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div#clickable {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 120px;
|
||||
}
|
||||
|
||||
div#avatar {
|
||||
background-color: #f8f8f8;
|
||||
border: 1px dashed silver;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +1,14 @@
|
||||
@(repositories: List[service.RepositoryService.RepositoryInfo], systemSettings: service.SystemSettingsService.SystemSettings,
|
||||
userRepositories: List[String])(implicit context: app.Context)
|
||||
@(activities: List[model.Activity],
|
||||
recentRepositories: List[service.RepositoryService.RepositoryInfo],
|
||||
systemSettings: service.SystemSettingsService.SystemSettings,
|
||||
userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main("GitBucket"){
|
||||
@dashboard.html.tab()
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
<h3>Recent updated repositories</h3>
|
||||
@repositories.map { repository =>
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
<a href="@url(repository.owner)">@repository.owner</a>
|
||||
/
|
||||
<a href="@url(repository)">@repository.name</a>
|
||||
@if(repository.repository.isPrivate){
|
||||
<i class="icon-lock"></i>
|
||||
}
|
||||
</div>
|
||||
@if(repository.repository.description.isDefined){
|
||||
<div>@repository.repository.description</div>
|
||||
}
|
||||
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
|
||||
</div>
|
||||
}
|
||||
@helper.html.activities(activities)
|
||||
</div>
|
||||
<div class="span4">
|
||||
@if(loginAccount.isEmpty){
|
||||
@@ -29,15 +16,53 @@
|
||||
} else {
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="metal">Your repositories (@userRepositories.size)</th>
|
||||
<th class="metal">
|
||||
<div class="pull-right">
|
||||
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
|
||||
</div>
|
||||
Your repositories (@userRepositories.size)
|
||||
</th>
|
||||
</tr>
|
||||
@userRepositories.map { repositoryName =>
|
||||
@if(userRepositories.isEmpty){
|
||||
<tr>
|
||||
<td><a href="@path/@loginAccount.get.userName/@repositoryName">@repositoryName</a></td>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
} else {
|
||||
@userRepositories.map { repository =>
|
||||
<tr>
|
||||
<td>
|
||||
@if(repository.owner == loginAccount.get.userName){
|
||||
<a href="@url(repository)"><strong>@repository.name</strong></a>
|
||||
} else {
|
||||
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
}
|
||||
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th class="metal">
|
||||
Recent updated repositories
|
||||
</th>
|
||||
</tr>
|
||||
@if(recentRepositories.isEmpty){
|
||||
<tr>
|
||||
<td>No repositories</td>
|
||||
</tr>
|
||||
} else {
|
||||
@recentRepositories.map { repository =>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
29
src/main/twirl/issues/commentform.scala.html
Normal file
29
src/main/twirl/issues/commentform.scala.html
Normal file
@@ -0,0 +1,29 @@
|
||||
@(issue: model.Issue,
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@if(loginAccount.isDefined){
|
||||
<form method="POST" validate="true">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-comment-box">
|
||||
<div class="box-content">
|
||||
@helper.html.preview(repository, "", false, true, "width: 680px; height: 100px;")
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
|
||||
@if((!issue.isPullRequest || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
|
||||
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#action').click(function(){
|
||||
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
73
src/main/twirl/issues/commentlist.scala.html
Normal file
73
src/main/twirl/issues/commentlist.scala.html
Normal file
@@ -0,0 +1,73 @@
|
||||
@(issue: model.Issue,
|
||||
comments: List[model.IssueComment],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@comments.map { comment =>
|
||||
@if(comment.action != "close" && comment.action != "reopen"){
|
||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||
<div class="box-header-small">
|
||||
<i class="icon-comment"></i>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented
|
||||
<span class="pull-right">
|
||||
@datetime(comment.registeredDate)
|
||||
@if(comment.action != "commit" && comment.action != "merge" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
|
||||
@markdown(comment.content, repository, false, true)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "merge"){
|
||||
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
|
||||
<span class="label label-info">Merged</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code>
|
||||
@if(pullreq.get.requestUserName == repository.owner){
|
||||
<span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.branch)</span>
|
||||
} else {
|
||||
<span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span>
|
||||
}
|
||||
@datetime(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "close" || comment.action == "close_comment"){
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label label-important">Closed</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@if(issue.isPullRequest){
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the pull request @datetime(comment.registeredDate)
|
||||
} else {
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label label-success">Reopened</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('i.icon-pencil').click(function(){
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
$.get('@url(repository)/issue_comments/_data/' + id,
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
function(data){
|
||||
$('#commentContent-' + id).empty().html(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -5,25 +5,25 @@
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("New Issue - " + repository.owner + "/" + repository.name){
|
||||
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("issues", repository)
|
||||
@tab("", repository)
|
||||
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span9">
|
||||
<div class="box">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 650px;"/>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 600px;"/>
|
||||
<div>
|
||||
<span id="label-assigned">No one is assigned</span>
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="assignedUserName" value=""/>
|
||||
@helper.html.dropdown {
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
<li class="divider"></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i> @collaborator</a></li>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,21 +31,37 @@
|
||||
<span id="label-milestone">No milestone</span>
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="milestoneId" value=""/>
|
||||
@helper.html.dropdown {
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
|
||||
<li class="divider"></li>
|
||||
@milestones.map { milestone =>
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId"><i class="icon-while"></i> @milestone.title</a></li>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
<i class="icon-while"></i> @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
@helper.html.preview(repository, "", false, true, true, "width: 650px; height: 200px;")
|
||||
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
<div class="pull-right">
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span3">
|
||||
@if(hasWritePermission){
|
||||
@@ -88,7 +104,7 @@ $(function(){
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).text();
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
$('a.milestone i.icon-ok').attr('class', 'icon-white');
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
<span id="error-edit-content-@commentId" class="error"></span>
|
||||
<textarea style="width: 730px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
<input type="button" class="btn btn-small" value="Update Comment"/>
|
||||
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
|
||||
<script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
<span id="error-edit-title" class="error"></span>
|
||||
<input type="text" style="width: 730px;" id="edit-title" value="@title"/>
|
||||
<textarea style="width: 730px; height: 100px;" id="edit-content">@content.getOrElse("")</textarea>
|
||||
<input type="text" style="width: 680px;" id="edit-title" value="@title"/>
|
||||
<textarea style="width: 680px; height: 100px;" id="edit-content">@content.getOrElse("")</textarea>
|
||||
<input type="button" class="btn btn-small" value="Update Issue"/>
|
||||
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
|
||||
<script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user