mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 18:16:59 +02:00
Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd84b3f1c4 | ||
|
|
9d4a052ecc | ||
|
|
f93c8965be | ||
|
|
beef86ce8c | ||
|
|
03b75d5379 | ||
|
|
66855e65bb | ||
|
|
b8da93912f | ||
|
|
d675115615 | ||
|
|
0296a0bde6 | ||
|
|
8409384232 | ||
|
|
cfaee56a08 | ||
|
|
7d65717784 | ||
|
|
7079d50fdf | ||
|
|
41a613e151 | ||
|
|
1f2b6a0acc | ||
|
|
25d402c9d1 | ||
|
|
045b7cf019 | ||
|
|
57109dd72e | ||
|
|
1b878b59b8 | ||
|
|
80452ab4cd | ||
|
|
4d9c8e8d3e | ||
|
|
a5f12a50e6 | ||
|
|
07ef06ad95 | ||
|
|
34e2663492 | ||
|
|
8b90f87589 | ||
|
|
8c1e45da6c | ||
|
|
62a6d74393 | ||
|
|
cb94447290 | ||
|
|
e4cf509d0f | ||
|
|
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 | ||
|
|
fc7481c60c | ||
|
|
4e8c130cbf | ||
|
|
f34f60b255 | ||
|
|
3c2675fd0d | ||
|
|
f163e348e0 | ||
|
|
71a3d79c82 | ||
|
|
bd1ba67647 | ||
|
|
60cd1320d2 | ||
|
|
a31de89f9c | ||
|
|
a129f53e0c | ||
|
|
6aa86ac2e3 | ||
|
|
28cafbcad2 | ||
|
|
991f60ce44 | ||
|
|
5dbeabcc58 | ||
|
|
b6bcebc588 | ||
|
|
2f3aa57d23 | ||
|
|
a123774bab | ||
|
|
7f56b50267 | ||
|
|
386f0dc142 | ||
|
|
bf90811cef | ||
|
|
72e2c6dca7 | ||
|
|
81fe467b20 | ||
|
|
d59e358caa | ||
|
|
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 | ||
|
|
bdd83a84fd | ||
|
|
ef38855b4b | ||
|
|
9bc8db5a15 | ||
|
|
c53f3843b8 | ||
|
|
56f1f5d47f | ||
|
|
d74ef599d3 | ||
|
|
398c77e277 | ||
|
|
99e562e9e6 | ||
|
|
47bdb8da23 | ||
|
|
869930165c | ||
|
|
d0f052e056 | ||
|
|
afd2325678 |
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.
|
||||
36
README.md
36
README.md
@@ -1,7 +1,7 @@
|
||||
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:
|
||||
|
||||
@@ -9,12 +9,12 @@ The current version of GitBucket provides a basic features below:
|
||||
- Repository viewer (some advanced features are not implemented)
|
||||
- Wiki
|
||||
- Issues
|
||||
- Activity timeline
|
||||
- User management (for Administrators)
|
||||
|
||||
Following features are not implemented, but we will make them in the future release!
|
||||
|
||||
- Fork and pull request
|
||||
- Timeline
|
||||
- Search
|
||||
- Network graph
|
||||
- Statics
|
||||
@@ -32,9 +32,39 @@ Installation
|
||||
|
||||
The default administrator account is **root** and password is **root**.
|
||||
|
||||
To upgrade GitBucket, only replace gitbucket.war.
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 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.
|
||||
|
||||
@@ -22,7 +22,7 @@ object MyBuild extends Build {
|
||||
scalaVersion := ScalaVersion,
|
||||
resolvers += Classpaths.typesafeReleases,
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "2.3.1.201302201838-r",
|
||||
"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",
|
||||
|
||||
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
||||
set SCRIPT_DIR=%~dp0
|
||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
|
||||
java -Dhttp.proxyHost=proxy.intellilink.co.jp -Dhttp.proxyPort=8080 -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\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: 473 B |
8
src/main/resources/update/1_1.sql
Normal file
8
src/main/resources/update/1_1.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Fix COLLABORATOR constraints
|
||||
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK1 IF EXISTS;
|
||||
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK0 IF EXISTS;
|
||||
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_PK IF EXISTS;
|
||||
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COLLABORATOR_NAME);
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
||||
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);
|
||||
@@ -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,7 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
context.mount(new LabelsController, "/*")
|
||||
context.mount(new MilestonesController, "/*")
|
||||
context.mount(new IssuesController, "/*")
|
||||
context.mount(new SettingsController, "/*")
|
||||
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, unique))),
|
||||
"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)))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text())))
|
||||
)(AccountNewForm.apply)
|
||||
|
||||
val editForm = mapping(
|
||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100)))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
)(AccountEditForm.apply)
|
||||
|
||||
/**
|
||||
@@ -33,43 +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
|
||||
}
|
||||
|
||||
private def unique: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getAccountByUserName(value).map { _ => "User already exists." }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,24 +1,58 @@
|
||||
package app
|
||||
|
||||
import model.Account
|
||||
import util.Validations
|
||||
import _root_.util.Directory._
|
||||
import _root_.util.{StringUtil, 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
|
||||
|
||||
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
|
||||
@@ -87,4 +121,91 @@ 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){
|
||||
|
||||
/**
|
||||
* 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(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getAccountByUserName(value).map { _ => "User already exists." }
|
||||
}
|
||||
|
||||
protected 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." }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,123 @@
|
||||
package app
|
||||
|
||||
import util.Directory._
|
||||
import util.UsersAuthenticator
|
||||
import util.{JGitUtil, UsersAuthenticator}
|
||||
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._
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 =>
|
||||
|
||||
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())))
|
||||
"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)
|
||||
|
||||
/**
|
||||
* 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
|
||||
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)
|
||||
createRepository(form.name, form.owner, form.description, form.isPrivate)
|
||||
|
||||
// Add collaborators for group repository
|
||||
if(ownerAccount.isGroupAccount){
|
||||
getGroupMembers(form.owner).foreach { userName =>
|
||||
addCollaborator(form.owner, form.name, userName)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert default labels
|
||||
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")
|
||||
createLabel(form.owner, form.name, "bug", "fc2929")
|
||||
createLabel(form.owner, form.name, "duplicate", "cccccc")
|
||||
createLabel(form.owner, form.name, "enhancement", "84b6eb")
|
||||
createLabel(form.owner, form.name, "invalid", "e6e6e6")
|
||||
createLabel(form.owner, form.name, "question", "cc317c")
|
||||
createLabel(form.owner, form.name, "wontfix", "ffffff")
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(loginUserName, form.name)
|
||||
val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build
|
||||
val gitdir = getRepositoryDir(form.owner, form.name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
repository.create
|
||||
if(form.createReadme){
|
||||
val tmpdir = getInitRepositoryDir(form.owner, form.name)
|
||||
try {
|
||||
// Clone the repository
|
||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
||||
|
||||
val config = repository.getConfig
|
||||
config.setBoolean("http", null, "receivepack", true)
|
||||
config.save
|
||||
// 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 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)
|
||||
val git = Git.open(tmpdir)
|
||||
git.add.addFilepattern("README.md").call
|
||||
git.commit.setCommitter(loginAccount.userName, loginAccount.mailAddress).setMessage("Initial commit").call
|
||||
git.push.call
|
||||
|
||||
} finally {
|
||||
FileUtils.deleteDirectory(tmpdir)
|
||||
}
|
||||
}
|
||||
|
||||
// Create Wiki repository
|
||||
createWikiRepository(context.loginAccount.get, form.name)
|
||||
createWikiRepository(loginAccount, form.owner, form.name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
|
||||
|
||||
// redirect to the repository
|
||||
redirect("/%s/%s".format(loginUserName, form.name))
|
||||
redirect(s"/${form.owner}/${form.name}")
|
||||
})
|
||||
|
||||
private def existsAccount: Constraint = new Constraint(){
|
||||
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.")
|
||||
params.get("owner").flatMap { userName =>
|
||||
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
57
src/main/scala/app/DashboardController.scala
Normal file
57
src/main/scala/app/DashboardController.scala
Normal file
@@ -0,0 +1,57 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.UsersAuthenticator
|
||||
|
||||
class DashboardController extends DashboardControllerBase
|
||||
with IssuesService with RepositoryService with AccountService
|
||||
with UsersAuthenticator
|
||||
|
||||
trait DashboardControllerBase extends ControllerBase {
|
||||
self: IssuesService 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")
|
||||
})
|
||||
|
||||
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, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open"), filterUser, repositories: _*),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, repositories: _*),
|
||||
condition),
|
||||
countIssue(condition, Map.empty, repositories: _*),
|
||||
countIssue(condition, Map("assigned" -> userName), repositories: _*),
|
||||
countIssue(condition, Map("created_by" -> userName), repositories: _*),
|
||||
countIssueGroupByRepository(condition, filterUser, repositories: _*),
|
||||
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)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -8,19 +8,18 @@ import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAut
|
||||
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,10 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
redirect("/%s/%s/issues/%d".format(owner, name, issueId))
|
||||
// record activity
|
||||
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
||||
@@ -112,28 +122,21 @@ 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 { id =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/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 { id =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
@@ -143,7 +146,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 +162,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 +179,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 +201,109 @@ 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(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 ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
|
||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
||||
|
||||
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 +313,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, (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, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, owner -> repoName),
|
||||
countIssue(condition, Map.empty, owner -> repoName),
|
||||
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), owner -> repoName)),
|
||||
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), 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(){
|
||||
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.repository))
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
@@ -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,14 +31,14 @@ 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"))
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -45,38 +46,38 @@ trait SettingsControllerBase extends ControllerBase {
|
||||
*/
|
||||
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))
|
||||
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,7 +97,7 @@ 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}")
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -104,17 +105,12 @@ trait SettingsControllerBase extends ControllerBase {
|
||||
*/
|
||||
private def collaborator: Constraint = new Constraint(){
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
@@ -200,7 +181,17 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
BadRequest
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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) get
|
||||
|
||||
(id, path.substring(id.length).replaceFirst("^/", ""))
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides HTML of the file list.
|
||||
*
|
||||
@@ -210,40 +201,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
|
||||
revisions.map { rev => (git.getRepository.resolve(rev), rev)}.find(_._1 != null).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 =>
|
||||
new String(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get, "UTF-8")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -24,20 +24,19 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
|
||||
}
|
||||
|
||||
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)
|
||||
getAccountByUserName(form.userName).collect {
|
||||
case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
|
||||
session.setAttribute("LOGIN_ACCOUNT", account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/%s".format(account.get.userName))
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
} getOrElse redirect("/signin")
|
||||
}
|
||||
|
||||
get("/signout"){
|
||||
|
||||
@@ -4,11 +4,12 @@ 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)
|
||||
@@ -19,11 +20,12 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
|
||||
|
||||
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))
|
||||
flash += "info" -> "System settings has been updated."
|
||||
redirect("/admin/system")
|
||||
})
|
||||
|
||||
|
||||
@@ -5,63 +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(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, unique))),
|
||||
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)))),
|
||||
"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)))),
|
||||
"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
|
||||
})
|
||||
|
||||
private def unique: 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
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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,27 +32,30 @@ 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 =>
|
||||
@@ -67,16 +72,20 @@ trait WikiControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
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(""))
|
||||
form.content, loginAccount, form.message.getOrElse(""))
|
||||
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordEditWikiPageActivity(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/_new")(collaboratorsOnly {
|
||||
@@ -84,20 +93,24 @@ 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"))
|
||||
|
||||
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, "Delete %s".format(pageName))
|
||||
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, 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 +120,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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -123,4 +139,16 @@ trait WikiControllerBase extends ControllerBase {
|
||||
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
|
||||
}
|
||||
|
||||
private def pagename: Constraint = new Constraint(){
|
||||
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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -2,7 +2,7 @@ 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")
|
||||
|
||||
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,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import Accounts._
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
|
||||
@@ -9,7 +8,10 @@ trait AccountService {
|
||||
|
||||
def getAccountByUserName(userName: String): Option[Account] =
|
||||
Query(Accounts) filter(_.userName is userName.bind) firstOption
|
||||
|
||||
|
||||
def getAccountByMailAddress(mailAddress: String): Option[Account] =
|
||||
Query(Accounts) filter(_.mailAddress is mailAddress.bind) firstOption
|
||||
|
||||
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
|
||||
|
||||
def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
|
||||
@@ -21,7 +23,9 @@ trait AccountService {
|
||||
url = url,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None)
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = false)
|
||||
|
||||
def updateAccount(account: Account): Unit =
|
||||
Accounts
|
||||
@@ -35,8 +39,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
|
||||
|
||||
}
|
||||
|
||||
115
src/main/scala/service/ActivityService.scala
Normal file
115
src/main/scala/service/ActivityService.scala
Normal file
@@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import Database.threadLocalSession
|
||||
|
||||
trait ActivityService {
|
||||
|
||||
def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = {
|
||||
val q = Query(Activities)
|
||||
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||
|
||||
(if(isPublic){
|
||||
q filter { case (t1, t2) => (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind) }
|
||||
} else {
|
||||
q filter { case (t1, t2) => t1.activityUserName is activityUserName.bind }
|
||||
})
|
||||
.sortBy { case (t1, t2) => t1.activityId desc }
|
||||
.map { case (t1, t2) => t1 }
|
||||
.take(30)
|
||||
.list
|
||||
}
|
||||
|
||||
def getRecentActivities(): List[Activity] =
|
||||
Query(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 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 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) =
|
||||
Activities.autoInc insert(userName, repositoryName, activityUserName,
|
||||
"edit_wiki",
|
||||
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
|
||||
Some(pageName),
|
||||
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_tag",
|
||||
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
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,22 @@ 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 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], 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) map (_.issueId) list).length
|
||||
}
|
||||
/**
|
||||
* Returns the Map which contains issue count for each labels.
|
||||
@@ -58,14 +59,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)
|
||||
.innerJoin(IssueLabels).on { (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
}
|
||||
@@ -80,73 +80,93 @@ 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 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],
|
||||
repos: (String, String)*): List[(String, String, Int)] = {
|
||||
searchIssueQuery(repos, condition.copy(repo = None), filterUser)
|
||||
.groupBy { t =>
|
||||
t.userName ~ t.repositoryName
|
||||
}
|
||||
.map { case (repo, t) =>
|
||||
repo ~ t.length
|
||||
}
|
||||
.filter (_._3 > 0.bind)
|
||||
.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" or "created_by", value is the user name)
|
||||
* @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],
|
||||
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)
|
||||
.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]) =
|
||||
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) } reduceLeft ( _ || _ ) ) &&
|
||||
(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) &&
|
||||
(IssueLabels filter { t2 =>
|
||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
|
||||
(t2.labelId in
|
||||
@@ -189,7 +209,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 +251,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 +315,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 +327,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 +336,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 +348,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 {
|
||||
|
||||
|
||||
125
src/main/scala/service/RepositorySearchService.scala
Normal file
125
src/main/scala/service/RepositorySearchService.scala
Normal file
@@ -0,0 +1,125 @@
|
||||
package service
|
||||
|
||||
import model.Issue
|
||||
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 = new String(bytes, "UTF-8")
|
||||
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 style=\"background-color: #ffff88;;\">$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,19 +11,17 @@ 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)
|
||||
*/
|
||||
def createRepository(repositoryName: String, userName: String, description: Option[String]): Unit = {
|
||||
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean): Unit = {
|
||||
Repositories insert
|
||||
Repository(
|
||||
userName = userName,
|
||||
repositoryName = repositoryName,
|
||||
isPrivate = false,
|
||||
isPrivate = isPrivate,
|
||||
description = description,
|
||||
defaultBranch = "master",
|
||||
registeredDate = currentDate,
|
||||
@@ -35,9 +32,15 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
|
||||
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
|
||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||
}
|
||||
|
||||
@@ -50,40 +53,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.
|
||||
*
|
||||
@@ -94,36 +63,48 @@ 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)
|
||||
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of accessible repositories information for the specified account user.
|
||||
*
|
||||
* @param account the account
|
||||
* @param baseUrl the base url of this application
|
||||
* @return the repository informations which is sorted in descending order of lastActivityDate.
|
||||
* Returns the list of specified user's repositories.
|
||||
* It contains own repositories and collaboration repositories.
|
||||
*/
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
(account match {
|
||||
/**
|
||||
* 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
|
||||
* @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 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +142,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.
|
||||
*
|
||||
@@ -185,6 +175,12 @@ trait RepositoryService { self: AccountService =>
|
||||
object RepositoryService {
|
||||
|
||||
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
|
||||
branchList: List[String], tags: List[util.JGitUtil.TagInfo])
|
||||
commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
|
||||
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository) = {
|
||||
this(repo.owner, repo.name, repo.url, model, repo.commitCount, repo.branchList, repo.tags)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/scala/service/RequestCache.scala
Normal file
26
src/main/scala/service/RequestCache.scala
Normal file
@@ -0,0 +1,26 @@
|
||||
package service
|
||||
|
||||
import model._
|
||||
|
||||
/**
|
||||
* 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 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
|
||||
@@ -64,18 +63,16 @@ object WikiService {
|
||||
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 = {
|
||||
lock(owner, repository){
|
||||
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 +83,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 +96,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 +104,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,10 +112,12 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,12 +205,16 @@ trait WikiService {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ object AutoUpdate {
|
||||
* Version of GitBucket
|
||||
*
|
||||
* @param majorVersion the major version
|
||||
* @param minorVersion the minor version
|
||||
* @param minorVersion the minor version
|
||||
*/
|
||||
case class Version(majorVersion: Int, minorVersion: Int){
|
||||
|
||||
@@ -22,10 +22,10 @@ object AutoUpdate {
|
||||
|
||||
/**
|
||||
* Execute update/MAJOR_MINOR.sql to update schema to this version.
|
||||
* If corresponding SQL file does not exist, this method do nothing.
|
||||
* 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")
|
||||
@@ -40,16 +40,36 @@ object AutoUpdate {
|
||||
}
|
||||
|
||||
/**
|
||||
* MAJOR.MINOR
|
||||
* 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, 0)
|
||||
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)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -84,7 +104,7 @@ object AutoUpdate {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start H2 database and update schema automatically.
|
||||
* Start H2 database and update schema automatically.
|
||||
*/
|
||||
class AutoUpdateListener extends org.h2.server.web.DbStarter {
|
||||
import AutoUpdate._
|
||||
@@ -109,6 +129,7 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
|
||||
} catch {
|
||||
case ex: Throwable => {
|
||||
logger.error("Failed to schema update", ex)
|
||||
ex.printStackTrace()
|
||||
conn.rollback()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,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 +32,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)
|
||||
}
|
||||
@@ -55,7 +60,7 @@ 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))
|
||||
account.password == sha1(password) && hasWritePermission(repository.owner, repository.name, Some(account))
|
||||
} getOrElse false
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,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 +60,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)
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ 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,7 +21,6 @@ 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"),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package util
|
||||
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
|
||||
/**
|
||||
* Provides directories used by GitBucket.
|
||||
@@ -13,13 +11,13 @@ object Directory {
|
||||
|
||||
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
||||
|
||||
val RepositoryHome = "%s/repositories".format(GitBucketHome)
|
||||
val RepositoryHome = s"${GitBucketHome}/repositories"
|
||||
|
||||
/**
|
||||
* 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 +31,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 +63,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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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
|
||||
|
||||
/**
|
||||
* Provides complex JGit operations.
|
||||
@@ -26,10 +27,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 +52,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 = {
|
||||
@@ -141,18 +150,35 @@ 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 i = git.log.all.call.iterator
|
||||
var commitCount = 0
|
||||
while(i.hasNext && commitCount <= 1000){
|
||||
i.next
|
||||
commitCount = commitCount + 1
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +260,7 @@ object JGitUtil {
|
||||
* @param path filters by this path. default is no filter.
|
||||
* @return a tuple of the commit list and whether has next
|
||||
*/
|
||||
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,20 +274,25 @@ 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).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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,51 +348,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 = git.getRepository.resolve(revision)
|
||||
paths.map { path =>
|
||||
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
|
||||
(path, commit)
|
||||
}.toMap
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -483,4 +474,22 @@ 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 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
package util
|
||||
|
||||
import java.net.{URLDecoder, URLEncoder}
|
||||
|
||||
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("\"", """)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package util
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import scala.Some
|
||||
|
||||
trait Validations {
|
||||
|
||||
@@ -11,9 +10,9 @@ trait Validations {
|
||||
def identifier: Constraint = new Constraint(){
|
||||
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
|
||||
}
|
||||
|
||||
36
src/main/scala/view/AvatarImageProvider.scala
Normal file
36
src/main/scala/view/AvatarImageProvider.scala
Normal file
@@ -0,0 +1,36 @@
|
||||
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){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${userName}/_avatar"""
|
||||
}
|
||||
} getOrElse {
|
||||
if(mailAddress.nonEmpty){
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +32,6 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
|
||||
enableWikiLink: Boolean) extends LinkRenderer {
|
||||
override def render(node: WikiLinkNode): Rendering = {
|
||||
if(enableWikiLink){
|
||||
super.render(node)
|
||||
} else {
|
||||
try {
|
||||
val text = node.getText
|
||||
val (label, page) = if(text.contains('|')){
|
||||
@@ -35,12 +40,13 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
|
||||
} else {
|
||||
(text, text)
|
||||
}
|
||||
val url = "%s/%s/%s/wiki/%s".format(context.path, repository.owner, repository.name,
|
||||
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("\"/>")
|
||||
@@ -96,8 +91,11 @@ class GitBucketHtmlSerializer(
|
||||
}
|
||||
|
||||
private def fixUrl(url: String): String = {
|
||||
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")) url
|
||||
else repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
|
||||
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")){
|
||||
url
|
||||
} else {
|
||||
repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
|
||||
}
|
||||
}
|
||||
|
||||
private def printAttribute(name: String, value: String) {
|
||||
@@ -105,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,63 @@ 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 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("\\[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,45 @@
|
||||
} else {
|
||||
<h3>Create your account</h3>
|
||||
}
|
||||
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{/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)
|
||||
@helper.html.information(info)
|
||||
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true">
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/main/twirl/account/repositories.scala.html
Normal file
25
src/main/twirl/account/repositories.scala.html
Normal file
@@ -0,0 +1,25 @@
|
||||
@(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.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>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,8 +1,9 @@
|
||||
@(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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
56
src/main/twirl/admin/users/user.scala.html
Normal file
56
src/main/twirl/admin/users/user.scala.html
Normal file
@@ -0,0 +1,56 @@
|
||||
@(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>
|
||||
<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>
|
||||
}
|
||||
8
src/main/twirl/dashboard/tab.scala.html
Normal file
8
src/main/twirl/dashboard/tab.scala.html
Normal file
@@ -0,0 +1,8 @@
|
||||
@(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 == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
|
||||
}
|
||||
</ul>
|
||||
45
src/main/twirl/helper/activities.scala.html
Normal file
45
src/main/twirl/helper/activities.scala.html
Normal file
@@ -0,0 +1,45 @@
|
||||
@(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">
|
||||
<div class="muted small">@datetime(activity.activityDate)</div>
|
||||
<div class="strong">
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
</div>
|
||||
@activity.additionalInfo.map { additionalInfo =>
|
||||
@if(additionalInfo.nonEmpty){
|
||||
@(activity.activityType match {
|
||||
case "create_wiki" => {
|
||||
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
|
||||
}
|
||||
case "edit_wiki" => {
|
||||
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
|
||||
}
|
||||
case "push" => {
|
||||
<div class="small activity-message">
|
||||
{additionalInfo.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 _ => {
|
||||
<div class=" activity-message">{additionalInfo}</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
@(body: Html)
|
||||
@(buttonValue: String = "")(body: Html)
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="icon-cog"></i>
|
||||
@if(buttonValue == ""){
|
||||
<i class="icon-cog"></i>
|
||||
} else {
|
||||
<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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -105,13 +121,15 @@ $(function(){
|
||||
if($(this).data('selected') == true){
|
||||
$(this).css({
|
||||
'background-color': 'white',
|
||||
'color' : 'black'
|
||||
'color' : 'black',
|
||||
'font-weight' : 'normal'
|
||||
});
|
||||
$(this).data('selected', false);
|
||||
} else {
|
||||
$(this).css({
|
||||
'background-color': '#' + $(this).data('bgcolor'),
|
||||
'color' : '#' + $(this).data('fgcolor')
|
||||
'color' : '#' + $(this).data('fgcolor'),
|
||||
'font-weight' : 'bold'
|
||||
});
|
||||
$(this).data('selected', true);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
comments: List[model.IssueComment],
|
||||
issueLabels: List[model.Label],
|
||||
collaborators: List[String],
|
||||
milestones: List[model.Milestone],
|
||||
milestones: List[(model.Milestone, Int, Int)],
|
||||
labels: List[model.Label],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("%s - Issue #%d - %s/%s".format(issue.title, issue.issueId, repository.owner, repository.name)){
|
||||
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("issues", repository)
|
||||
@tab("issues", repository)
|
||||
<ul class="nav nav-tabs">
|
||||
@@ -17,7 +17,8 @@
|
||||
</ul>
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
<div class="box">
|
||||
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content" style="padding: 0px;">
|
||||
<div class="issue-header">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
@@ -31,82 +32,115 @@
|
||||
<div class="issue-info">
|
||||
<span id="label-assigned">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
<a href="@url(userName)" class="username strong">@userName</a> is assigned
|
||||
@avatar(userName, 20) <a href="@url(userName)" class="username strong">@userName</a> is assigned
|
||||
}.getOrElse("No one is assigned")
|
||||
</span>
|
||||
@if(hasWritePermission){
|
||||
@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-white"></i> @collaborator</a></li>
|
||||
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
<div class="pull-right">
|
||||
<span id="label-milestone">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@milestones.find(_.milestoneId == milestoneId).map { milestone =>
|
||||
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
|
||||
Milestone: <strong>@milestone.title</strong>
|
||||
}
|
||||
}.getOrElse("No milestone")
|
||||
</span>
|
||||
<div id="milestone-progress-area">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
|
||||
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
@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-white"></i> @milestone.title</a></li>
|
||||
@milestones.map { case (milestone, _, _) =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
<i class="icon-white"></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>
|
||||
<div class="issue-content" id="issueContent">
|
||||
@markdown(issue.content getOrElse "No description given.", repository, false, true, true)
|
||||
@markdown(issue.content getOrElse "No description given.", repository, false, true)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="issue-participants">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<strong>@participants.size</strong> @plural(participants.size, "participant")
|
||||
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
|
||||
}
|
||||
</div>
|
||||
@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(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
|
||||
@if(comment.action != "commit" && (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, true)
|
||||
@markdown(comment.content, repository, false, true)
|
||||
</div>
|
||||
</div>
|
||||
@comment.action.map { action =>
|
||||
}
|
||||
@if(comment.action == "close" || comment.action == "close_comment"){
|
||||
<div class="small issue-comment-action">
|
||||
@if(action == "close"){
|
||||
<span class="label label-important">Closed</span>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
|
||||
} else {
|
||||
<span class="label label-success">Reopened</span>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
|
||||
}
|
||||
<span class="label label-important">Closed</span>
|
||||
<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>
|
||||
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if(loginAccount.isDefined){
|
||||
<form action="@url(repository)/issue_comments/new" method="POST" validate="true">
|
||||
<div class="box">
|
||||
<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, true, "width: 730px; height: 100px;")
|
||||
@helper.html.preview(repository, "", false, true, "width: 680px; height: 100px;")
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||
<input type="submit" class="btn btn-success" value="Comment"/>
|
||||
@if(hasWritePermission || issue.openedUserName == loginAccount.get.userName){
|
||||
<input type="submit" class="btn" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
||||
}
|
||||
<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(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>
|
||||
}
|
||||
</div>
|
||||
@@ -117,14 +151,16 @@
|
||||
<span class="label label-success issue-status">Open</span>
|
||||
}
|
||||
<div class="small" style="text-align: center;">
|
||||
<strong>@comments.size</strong> @plural(comments.size, "comment")
|
||||
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
|
||||
<strong>@count</strong> @plural(count, "comment")
|
||||
}
|
||||
</div>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong>Labels</strong>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown {
|
||||
@helper.html.dropdown() {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||
@@ -134,7 +170,6 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -166,7 +201,8 @@ $(function(){
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var userName = $(this).data('name');
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
$.post('@url(repository)/issues/@issue.issueId/assign',
|
||||
{
|
||||
assignedUserName: userName
|
||||
@@ -176,27 +212,31 @@ $(function(){
|
||||
if(userName == ''){
|
||||
$('#label-assigned').text('No one is assigned');
|
||||
} else {
|
||||
$('#label-assigned').html($('<span>')
|
||||
$('#label-assigned').empty()
|
||||
.append($this.find('img.avatar').clone(false)).append(' ')
|
||||
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
|
||||
.append(' is assigned'));
|
||||
.append(' is assigned');
|
||||
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).text();
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
$.post('@url(repository)/issues/@issue.issueId/milestone',
|
||||
{
|
||||
milestoneId: milestoneId
|
||||
},
|
||||
function(){
|
||||
function(data){
|
||||
console.log(data);
|
||||
$('a.milestone i.icon-ok').attr('class', 'icon-white');
|
||||
if(milestoneId == ''){
|
||||
$('#label-milestone').text('No milestone');
|
||||
$('#milestone-progress-area').empty();
|
||||
} else {
|
||||
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
|
||||
$('#milestone-progress-area').html(data);
|
||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@(issues: List[(model.Issue, List[model.Label], Int)],
|
||||
page: Int,
|
||||
labels: List[model.Label],
|
||||
collaborators: List[String],
|
||||
milestones: List[model.Milestone],
|
||||
labels: List[model.Label],
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
allCount: Int,
|
||||
@@ -14,7 +15,7 @@
|
||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Issues - " + repository.owner + "/" + repository.name){
|
||||
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.header("issues", repository)
|
||||
@tab("issues", repository)
|
||||
<div class="row-fluid">
|
||||
@@ -51,7 +52,7 @@
|
||||
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown {
|
||||
@helper.html.dropdown() {
|
||||
@if(condition.milestoneId.isDefined){
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = None).toURL">
|
||||
@@ -64,21 +65,45 @@
|
||||
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
|
||||
</a>
|
||||
</li>
|
||||
@milestones.map { milestone =>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
|
||||
</a>
|
||||
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @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>
|
||||
}
|
||||
}
|
||||
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
|
||||
<div class="milestone-progress" style="margin-top: 8px;">
|
||||
@if(closedCount > 0){
|
||||
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
|
||||
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
|
||||
<div style="margin-top: 4px;">
|
||||
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
|
||||
</div>
|
||||
<span class="muted small">@openCount open issues</span>
|
||||
@if(milestone.closedDate.isDefined){
|
||||
@milestone.closedDate.map { closedDate =>
|
||||
<span class="small">Closed in @date(closedDate)</span>
|
||||
}
|
||||
} else {
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="small">Due in @date(dueDate)</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<span class="muted small">@openCount open issues</span>
|
||||
}
|
||||
}
|
||||
<hr/>
|
||||
<strong>Labels</strong>
|
||||
@@ -106,99 +131,15 @@
|
||||
@_root_.issues.labels.html.edit(None, repository)
|
||||
}
|
||||
</div>
|
||||
<div class="span9">
|
||||
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
|
||||
<i class="icon-remove-circle"></i> Clear milestone and label filters
|
||||
</a>
|
||||
}
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
|
||||
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
Sort:
|
||||
<strong>
|
||||
@if(condition.sort == "created" && condition.direction == "desc"){ Newest }
|
||||
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest }
|
||||
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented }
|
||||
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented }
|
||||
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated }
|
||||
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated }
|
||||
</strong>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
@issues.map { case (issue, labels, commentCount) =>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@url(repository)/issues/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
@labels.map { label =>
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right muted">#@issue.issueId</span>
|
||||
<div class="small muted">
|
||||
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)
|
||||
<i class="icon-comment"></i><a href="@url(repository)/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if(issues.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No issues to show.
|
||||
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
|
||||
} else {
|
||||
<a href="@url(repository)/issues/new">Create a new issue.</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
|
||||
</div>
|
||||
</div>
|
||||
@***** show issue list *****@
|
||||
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<form id="batcheditForm" method="POST">
|
||||
<input type="hidden" name="value"/>
|
||||
<input type="hidden" name="checked"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@if(hasWritePermission){
|
||||
<script>
|
||||
@@ -213,6 +154,33 @@ $(function(){
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('.table-issues input[type=checkbox]').change(function(){
|
||||
$('.table-issues button').prop('disabled',
|
||||
!$('.table-issues input[type=checkbox]').filter(':checked').length);
|
||||
}).filter(':first').change();
|
||||
|
||||
var submitBatchEdit = function(action, value) {
|
||||
var checked = $('.table-issues input[type=checkbox]').filter(':checked').map(function(){ return this.value; }).get().join();
|
||||
var form = $('#batcheditForm');
|
||||
form.find('input[name=value]').val(value);
|
||||
form.find('input[name=checked]').val(checked);
|
||||
form.attr('action', action);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
$('#state').click(function(){
|
||||
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase());
|
||||
});
|
||||
$('a.toggle-label').click(function(){
|
||||
submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id'));
|
||||
});
|
||||
$('a.toggle-assign').click(function(){
|
||||
submitBatchEdit('@url(repository)/issues/batchedit/assign', $(this).data('name'));
|
||||
});
|
||||
$('a.toggle-milestone').click(function(){
|
||||
submitBatchEdit('@url(repository)/issues/batchedit/milestone', $(this).data('id'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
176
src/main/twirl/issues/listparts.scala.html
Normal file
176
src/main/twirl/issues/listparts.scala.html
Normal file
@@ -0,0 +1,176 @@
|
||||
@(issues: List[(model.Issue, List[model.Label], Int)],
|
||||
page: Int,
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
collaborators: List[String] = Nil,
|
||||
milestones: List[model.Milestone] = Nil,
|
||||
labels: List[model.Label] = Nil,
|
||||
repository: Option[service.RepositoryService.RepositoryInfo] = None,
|
||||
hasWritePermission: Boolean = false)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
|
||||
<div class="span9">
|
||||
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
|
||||
<i class="icon-remove-circle"></i> Clear milestone and label filters
|
||||
</a>
|
||||
}
|
||||
@if(condition.repo.isDefined){
|
||||
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
|
||||
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
|
||||
</a>
|
||||
}
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
|
||||
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
Sort:
|
||||
<strong>
|
||||
@if(condition.sort == "created" && condition.direction == "desc"){ Newest }
|
||||
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest }
|
||||
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented }
|
||||
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented }
|
||||
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated }
|
||||
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated }
|
||||
</strong>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
@if(issues.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No issues to show.
|
||||
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
|
||||
} else {
|
||||
@if(repository.isDefined){
|
||||
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
} else {
|
||||
@if(hasWritePermission){
|
||||
<tr>
|
||||
<td style="background-color: #eee;">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-mini" id="state"><strong>@{if(condition.state == "open") "Close" else "Reopen"}</strong></button>
|
||||
</div>
|
||||
@helper.html.dropdown("Label") {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
|
||||
<i class="icon-white"></i>
|
||||
<span class="label" style="background-color: #@label.color;"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Assignee") {
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Milestone") {
|
||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
|
||||
@milestones.map { milestone =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
|
||||
<i class="icon-white"></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>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@issues.map { case (issue, labels, commentCount) =>
|
||||
<tr>
|
||||
<td>
|
||||
@if(hasWritePermission){
|
||||
<label class="checkbox" style="cursor: default;">
|
||||
<input type="checkbox" value="@issue.issueId"/>
|
||||
}
|
||||
@if(repository.isEmpty){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
||||
}
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
@labels.map { label =>
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right muted">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
#@issue.issueId
|
||||
</span>
|
||||
<div class="small muted">
|
||||
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Milestones - " + repository.owner + "/" + repository.name){
|
||||
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||
@html.header("milestones", repository)
|
||||
@issues.html.tab("milestones", repository)
|
||||
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Milestones - " + repository.owner + "/" + repository.name){
|
||||
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||
@html.header("milestones", repository)
|
||||
@issues.html.tab("milestones", repository)
|
||||
<div class="row-fluid">
|
||||
@@ -42,9 +42,13 @@
|
||||
@if(milestone.closedDate.isDefined){
|
||||
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
|
||||
} else {
|
||||
@if(milestone.dueDate.isDefined){
|
||||
<span class="muted">Due in @date(milestone.dueDate.get)</span>
|
||||
} else {
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
}
|
||||
@@ -65,23 +69,12 @@
|
||||
</div>
|
||||
<span class="muted">@closedCount closed - @openCount open</span>
|
||||
</div>
|
||||
<div class="milestone-progress">
|
||||
@if(closedCount > 0){
|
||||
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
|
||||
}
|
||||
<span class="milestone-percentage">
|
||||
@if(closedCount == 0){
|
||||
0%
|
||||
} else {
|
||||
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@progress(openCount + closedCount, closedCount, true)
|
||||
</div>
|
||||
</div>
|
||||
@if(milestone.description.isDefined){
|
||||
<div class="milestone-description">
|
||||
@markdown(milestone.description.get, repository, false, false, false)
|
||||
@markdown(milestone.description.get, repository, false, false)
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
|
||||
15
src/main/twirl/issues/milestones/progress.scala.html
Normal file
15
src/main/twirl/issues/milestones/progress.scala.html
Normal file
@@ -0,0 +1,15 @@
|
||||
@(total: Int, progress: Int, showPercentage: Boolean)
|
||||
<div class="milestone-progress">
|
||||
@if(progress > 0){
|
||||
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
|
||||
}
|
||||
@if(showPercentage){
|
||||
<span class="milestone-percentage">
|
||||
@if(progress == 0){
|
||||
0%
|
||||
} else {
|
||||
@((progress.toDouble / total.toDouble * 100).toInt)%
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
@(title: String)(body: Html)(implicit context: app.Context)
|
||||
@(title: String, repository: Option[service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<!DOCTYPE html>
|
||||
@@ -19,45 +19,60 @@
|
||||
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
|
||||
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
|
||||
<script src="@assets/common/js/jquery-1.9.1.js"></script>
|
||||
<script src="@assets/common/js/dropzone.js"></script>
|
||||
<script src="@assets/common/js/validation.js"></script>
|
||||
<script src="@assets/common/js/gitbucket.js"></script>
|
||||
<script src="@assets/bootstrap/js/bootstrap.js"></script>
|
||||
<script src="@assets/datepicker/js/bootstrap-datepicker.js"></script>
|
||||
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script>
|
||||
<script src="@assets/google-code-prettify/prettify.js"></script>
|
||||
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar navbar-inverse">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="brand" href="@path/">GitBucket</a>
|
||||
<div class="nav-collapse collapse">
|
||||
<ul class="nav pull-right">
|
||||
@if(loginAccount.isDefined){
|
||||
<li><a href="@path/new">New repo</a></li>
|
||||
<li><a href="@url(loginAccount.get.userName)">Account</a></li>
|
||||
@if(loginAccount.get.isAdmin){
|
||||
<li><a href="@path/admin/users">Administration</a></li>
|
||||
}
|
||||
<li><a href="@path/signout">Sign out</a></li>
|
||||
} else {
|
||||
<li><a href="@path/signin?@currentUrl">Sign in</a></li>
|
||||
<form id="search" action="@path/search" method="POST">
|
||||
<div class="navbar">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="brand" href="@path/">GitBucket</a>
|
||||
<div class="nav-collapse collapse pull-right header-menu">
|
||||
@repository.map { repository =>
|
||||
<input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
|
||||
<input type="hidden" name="owner" value="@repository.owner"/>
|
||||
<input type="hidden" name="repository" value="@repository.name"/>
|
||||
}
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
@if(loginAccount.isDefined){
|
||||
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
|
||||
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
|
||||
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
|
||||
@if(loginAccount.get.isAdmin){
|
||||
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
|
||||
}
|
||||
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
|
||||
} else {
|
||||
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
|
||||
}
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
|
||||
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
|
||||
}
|
||||
</div>
|
||||
<div class="container body">
|
||||
@body
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#search').submit(function(){
|
||||
return $.trim($(this).find('input[name=query]').val()) != '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
<script>
|
||||
$(function(){ prettyPrint(); });
|
||||
</script>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,18 +1,73 @@
|
||||
@()(implicit context: app.Context)
|
||||
@(groupNames: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main("Create a New Repository"){
|
||||
<form id="form" method="post" action="@path/new" validate="true">
|
||||
<fieldset>
|
||||
<label for="name"><strong>Repository name</strong></label>
|
||||
<input type="text" name="name" id="name" />
|
||||
<span id="error-name" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="description"><strong>Description</strong> (optional)</label>
|
||||
<input type="text" name="description" id="description" style="width: 600px;"/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Create repository"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
<div style="width: 600px; margin: 10px auto;">
|
||||
<form id="form" method="post" action="@path/new" validate="true">
|
||||
<fieldset>
|
||||
<label for="name"><strong>Repository name</strong></label>
|
||||
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
<strong>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</strong>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="icon-ok"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
|
||||
@groupNames.map { groupName =>
|
||||
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
|
||||
}
|
||||
</ul>
|
||||
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
|
||||
</div>
|
||||
/
|
||||
<input type="text" name="name" id="name" />
|
||||
<span id="error-name" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="description"><strong>Description</strong> (optional)</label>
|
||||
<input type="text" name="description" id="description" style="width: 95%;"/>
|
||||
</fieldset>
|
||||
<fieldset class="margin">
|
||||
<label>
|
||||
<input type="radio" name="isPrivate" value="false" checked>
|
||||
<strong>Public</strong><br>
|
||||
<div>
|
||||
<span class="note">All users and guests can read this repository.</span>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input type="radio" name="isPrivate" value="true">
|
||||
<strong>Private</strong><br>
|
||||
<div>
|
||||
<span class="note">Only collaborators can read this repository.</span>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="margin">
|
||||
<label for="createReadme">
|
||||
<input type="checkbox" name="createReadme" id="createReadme"/>
|
||||
<strong>Initialize this repository with a README</strong>
|
||||
<div>
|
||||
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="margin">
|
||||
<input type="submit" class="btn btn-success" value="Create repository"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
<script>
|
||||
$('#owner-dropdown a').click(function(){
|
||||
var userName = $(this).data('name');
|
||||
$('#owner').val(userName);
|
||||
|
||||
$('#owner-dropdown i').attr('class', 'icon-white');
|
||||
$(this).find('i').attr('class', 'icon-ok');
|
||||
|
||||
$('#owner-dropdown strong').html($(this).find('span').html());
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(repository.owner+"/"+repository.name) {
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.header("code", repository)
|
||||
@tab(branch, repository, "files")
|
||||
<div class="head">
|
||||
@@ -23,6 +23,7 @@
|
||||
<tr>
|
||||
<th style="font-weight: normal;">
|
||||
<div class="pull-left">
|
||||
@avatar(latestCommit, 20)
|
||||
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
|
||||
<span class="muted">@datetime(latestCommit.time)</span>
|
||||
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@import view.helpers._
|
||||
@import util.Implicits._
|
||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||
@html.main(commit.shortMessage){
|
||||
@html.main(commit.shortMessage, Some(repository)){
|
||||
@html.header("code", repository)
|
||||
@tab(commitId, repository, "commits")
|
||||
<table class="table table-bordered">
|
||||
@@ -43,7 +43,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@url(commit.committer)" class="username strong">@commit.committer</a> <span class="muted">@datetime(commit.time)</span>
|
||||
@avatar(commit, 20)
|
||||
<a href="@url(commit.committer)" class="username strong">@commit.committer</a>
|
||||
<span class="muted">@datetime(commit.time)</span>
|
||||
<div class="pull-right monospace small" style="text-align: right;">
|
||||
<div>
|
||||
@if(commit.parents.size == 0){
|
||||
@@ -72,11 +74,14 @@
|
||||
<div class="pull-right" style="margin-bottom: 10px;">
|
||||
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
|
||||
</div>
|
||||
Showing @diffs.size changed @plural(diffs.size, "file")
|
||||
@*
|
||||
@if(diffs.size == 1){
|
||||
Showing 1 changed files
|
||||
Showing 1 changed file
|
||||
} else {
|
||||
Showing @diffs.size changed files
|
||||
}
|
||||
*@
|
||||
</div>
|
||||
<ul id="commit-file-list" style="display: none;">
|
||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
hasNext: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(repository.owner+"/"+repository.name) {
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.header("code", repository)
|
||||
@tab(branch, repository, if(pathList.isEmpty) "commits" else "files")
|
||||
<div class="head">
|
||||
@@ -33,24 +33,27 @@
|
||||
@day.map { commit =>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="pull-left">
|
||||
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
|
||||
@if(commit.description.isDefined){
|
||||
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
|
||||
}
|
||||
<br>
|
||||
@if(commit.description.isDefined){
|
||||
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
|
||||
}
|
||||
<div class="small">
|
||||
<a href="@url(commit.committer)" class="username">@commit.committer</a>
|
||||
<span class="muted">@datetime(commit.time)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right align-right">
|
||||
<a href="@url(repository)/commit/@commit.id" class="btn btn-small monospace">@commit.id.substring(0, 10)</a><br>
|
||||
<a href="@url(repository)/tree/@commit.id" class="small">Browse code</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="commit-avatar-image">@avatar(commit, 40)</div>
|
||||
<div class="commit-message-box">
|
||||
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
|
||||
@if(commit.description.isDefined){
|
||||
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
|
||||
}
|
||||
<br>
|
||||
@if(commit.description.isDefined){
|
||||
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
|
||||
}
|
||||
<div class="small">
|
||||
<a href="@url(commit.committer)" class="username">@commit.committer</a>
|
||||
<span class="muted">@datetime(commit.time)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
readme: Option[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(repository.owner + "/" + repository.name) {
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.header("code", repository)
|
||||
@tab(branch, repository, "files")
|
||||
<div class="head">
|
||||
<div class="pull-right">
|
||||
@defining(repository.commitCount){ commitCount =>
|
||||
<a href="@url(repository)/commits/@branch">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>
|
||||
}
|
||||
</div>
|
||||
<a href="@url(repository)/tree/@branch">@repository.name</a> /
|
||||
@pathList.zipWithIndex.map { case (section, i) =>
|
||||
<a href="@url(repository)/tree/@branch/@pathList.take(i + 1).mkString("/")">@section</a> /
|
||||
@@ -22,8 +27,6 @@
|
||||
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
|
||||
@if(latestCommit.description.isDefined){
|
||||
<a href="javascript:void(0)" onclick="$('#description-@latestCommit.id').toggle();" class="omit">...</a>
|
||||
}
|
||||
@if(latestCommit.description.isDefined){
|
||||
<pre id="description-@latestCommit.id" class="commit-description" style="display: none;">@link(latestCommit.description.get, repository)</pre>
|
||||
}
|
||||
</th>
|
||||
@@ -31,6 +34,7 @@
|
||||
<tr>
|
||||
<td colspan="4" class="latest-commit">
|
||||
<div>
|
||||
@avatar(latestCommit, 20)
|
||||
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
|
||||
<span class="muted">@datetime(latestCommit.time)</span>
|
||||
<div class="pull-right align-right monospace">
|
||||
@@ -73,7 +77,7 @@
|
||||
@readme.map { content =>
|
||||
<div class="box">
|
||||
<div class="box-header">README.md</div>
|
||||
<div class="box-content">@markdown(content, repository, false, false, false)</div>
|
||||
<div class="box-content">@markdown(content, repository, false, false)</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
21
src/main/twirl/repo/guide.scala.html
Normal file
21
src/main/twirl/repo/guide.scala.html
Normal file
@@ -0,0 +1,21 @@
|
||||
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.header("code", repository)
|
||||
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
|
||||
<pre>
|
||||
touch README.md
|
||||
git init
|
||||
git add README.md
|
||||
git commit -m "first commit"
|
||||
git remote add origin @repository.url
|
||||
git push -u origin master
|
||||
</pre>
|
||||
|
||||
<h3 style="margin-top: 30px;">Push an existing repository from the command line</h3>
|
||||
<pre>
|
||||
git remote add origin @repository.url
|
||||
git push -u origin master
|
||||
</pre>
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
@(id: String, repository: service.RepositoryService.RepositoryInfo, active: String)(implicit context: app.Context)
|
||||
@(id: String, repository: service.RepositoryService.RepositoryInfo, active: String,
|
||||
hideBranchPulldown: Boolean = false)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<ul class="nav nav-tabs">
|
||||
@if(!hideBranchPulldown){
|
||||
<li>
|
||||
<div class="btn-group" style="margin-right: 20px;">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
@if(id.length == 40){
|
||||
tree: <strong>@id.substring(0, 10)</strong>
|
||||
} else {
|
||||
}
|
||||
@if(repository.branchList.contains(id)){
|
||||
branch: <strong>@id</strong>
|
||||
}
|
||||
@if(repository.tags.exists(_.name == id)){
|
||||
tag: <strong>@id</strong>
|
||||
}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
@@ -19,13 +25,14 @@
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li@if(active=="files"){ class="active"}><a href="@url(repository)/tree/@id">Files</a></li>
|
||||
}
|
||||
<li@if(active=="files" ){ class="active"}><a href="@url(repository)/tree/@id">Files</a></li>
|
||||
<li@if(active=="commits"){ class="active"}><a href="@url(repository)/commits/@id">Commits</a></li>
|
||||
<li@if(active=="tags"){ class="active"}><a href="@url(repository)/tags">Tags@if(repository.tags.length > 0){ <span class="badge">@repository.tags.length</span>}</a></li>
|
||||
<li@if(active=="tags" ){ class="active"}><a href="@url(repository)/tags">Tags@if(repository.tags.length > 0){ <span class="badge">@repository.tags.length</span>}</a></li>
|
||||
<li class="pull-right">
|
||||
<div class="input-prepend">
|
||||
<span class="add-on">HTTP</span>
|
||||
<div class="input-append">
|
||||
<input type="text" value="@repository.url" id="repository-url" readonly>
|
||||
<span id="repository-url-copy" class="add-on btn" data-clipboard-text="@repository.url" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(repository.owner + "/" + repository.name) {
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.header("code", repository)
|
||||
@tab("master", repository, "tags") @* TODO DON'T display branch pulldown *@
|
||||
@tab(repository.repository.defaultBranch, repository, "tags", true)
|
||||
<h1>Tags</h1>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
|
||||
26
src/main/twirl/search/code.scala.html
Normal file
26
src/main/twirl/search/code.scala.html
Normal file
@@ -0,0 +1,26 @@
|
||||
@(files: List[service.RepositorySearchService.FileSearchResult],
|
||||
issueCount: Int,
|
||||
query: String,
|
||||
page: Int,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@import service.RepositorySearchService._
|
||||
@html.main("Search Results", Some(repository)){
|
||||
@menu("code", files.size, issueCount, query, repository){
|
||||
@if(files.isEmpty){
|
||||
<h4>We couldn't find any code matching '@query'</h4>
|
||||
} else {
|
||||
<h4>We've found @files.size code @plural(files.size, "result")</h4>
|
||||
}
|
||||
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
|
||||
<div>
|
||||
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
|
||||
<div class="small muted">Latest commit at @datetime(file.lastModified)</div>
|
||||
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
|
||||
</div>
|
||||
}
|
||||
@helper.html.paginator(page, files.size, CodeLimit, 10,
|
||||
s"${url(repository)}/search?q=${urlEncode(query)}&type=code")
|
||||
}
|
||||
}
|
||||
35
src/main/twirl/search/issues.scala.html
Normal file
35
src/main/twirl/search/issues.scala.html
Normal file
@@ -0,0 +1,35 @@
|
||||
@(issues: List[service.RepositorySearchService.IssueSearchResult],
|
||||
fileCount: Int,
|
||||
query: String,
|
||||
page: Int,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@import service.RepositorySearchService._
|
||||
@html.main("Search Results", Some(repository)){
|
||||
@menu("issue", fileCount, issues.size, query, repository){
|
||||
@if(issues.isEmpty){
|
||||
<h4>We couldn't find any code matching '@query'</h4>
|
||||
} else {
|
||||
<h4>We've found @issues.size code @plural(issues.size, "result")</h4>
|
||||
}
|
||||
@issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue =>
|
||||
<div class="block">
|
||||
<div class="pull-right muted">#@issue.issueId</div>
|
||||
<h4 style="margin-top: 0px;"><a href="@url(repository)/issues/@issue.issueId">@issue.title</a></h4>
|
||||
@if(issue.highlightText.nonEmpty){
|
||||
<pre>@Html(issue.highlightText)</pre>
|
||||
}
|
||||
<div class="small muted">
|
||||
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
|
||||
at @datetime(issue.registeredDate)
|
||||
@if(issue.commentCount > 0){
|
||||
<i class="icon-comment"></i><strong>@issue.commentCount</strong> @plural(issue.commentCount, "comment")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@helper.html.paginator(page, issues.size, IssueLimit, 10,
|
||||
s"${url(repository)}/search?q=${urlEncode(query)}&type=issue")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user