Compare commits

...

106 Commits
1.1 ... 1.3

Author SHA1 Message Date
shimamoto
188237db24 Replace String#format() with string interpolation. 2013-07-18 19:54:07 +09:00
takezoe
323e25951f Use Gravatar if committer is not registered in GitBucket. 2013-07-18 13:40:21 +09:00
Naoki Takezoe
49d0c0de87 Update for 1.3 release. 2013-07-18 12:46:08 +09:00
takezoe
e31a835c4e (refs #30)Add LICENSE file 2013-07-18 12:34:06 +09:00
takezoe
fed4619a92 Merge remote-tracking branch 'origin/master' 2013-07-18 03:53:22 +09:00
takezoe
9eb1a20b3f Batch updating for issues did not work with IE (also IE10).
I applied quick fix to release 1.3. Please update after 1.3 release if you have better solution.
2013-07-18 03:52:46 +09:00
Naoki Takezoe
ac21b9cc20 Update README.md 2013-07-18 03:35:08 +09:00
Naoki Takezoe
4a486e3bf8 Update README.md 2013-07-18 03:34:30 +09:00
takezoe
269374e6bb Quote src attribute of avatar image. 2013-07-18 03:11:56 +09:00
shimamoto
8f056e4a82 Finished the issues controller refactoring. 2013-07-17 15:36:05 +09:00
shimamoto
d866847c0d (refs #11) Fix comments to display. 2013-07-17 13:56:54 +09:00
shimamoto
ac784f8905 (refs #11) Change the value to be set in the action. 2013-07-17 12:40:23 +09:00
shimamoto
6035281ca1 Change action(IssueComment) to String type. 2013-07-17 12:37:48 +09:00
takezoe
4572a455c8 Change ISSUE_COMMENT.ACTION to NOT NULL. 2013-07-16 23:05:45 +09:00
takezoe
0bc6102096 (refs #39)Remove unnecessary attribute. 2013-07-16 21:31:42 +09:00
takezoe
3282a8d76a (refs #39)Small fix for copy button. 2013-07-16 21:30:21 +09:00
Naoki Takezoe
d04befb8d0 Merge pull request #39 from tanacasino/feature/copy-to-clipboard
Add copy to clipboard git clone URL
2013-07-16 05:04:47 -07:00
Tomofumi Tanaka
fc7481c60c Add copy to clipboard clone URL 2013-07-16 01:10:52 +09:00
takezoe
4e8c130cbf Expand column COMMENT.ACTION to VARCHAR(20). 2013-07-12 16:07:20 +09:00
takezoe
f34f60b255 Returns a gray image if username has not been registered. 2013-07-12 15:43:23 +09:00
takezoe
3c2675fd0d Remove unused import statement. 2013-07-12 15:23:01 +09:00
takezoe
f163e348e0 (refs #34)Link conversion checks existence of accounts and issues. 2013-07-12 15:15:58 +09:00
takezoe
71a3d79c82 Fix commit list presentation. 2013-07-12 04:34:54 +09:00
takezoe
bd1ba67647 Add avatar to the blob view. 2013-07-12 04:29:27 +09:00
takezoe
60cd1320d2 Migration for wiki repository configuration in 1.3 which add http.receivepack=true. 2013-07-12 03:30:25 +09:00
takezoe
a31de89f9c Remove debug code. 2013-07-12 03:26:41 +09:00
takezoe
a129f53e0c Remove fixed TODO. 2013-07-12 03:01:28 +09:00
takezoe
6aa86ac2e3 Use StringUtil#urlEncode() instead of URLEncode#encode(). 2013-07-12 02:18:29 +09:00
takezoe
28cafbcad2 (refs #35)Fixed. 2013-07-12 02:14:27 +09:00
takezoe
991f60ce44 (refs #34)@xxxx in markdown as link. 2013-07-12 01:29:23 +09:00
takezoe
5dbeabcc58 Support formaction attribute. 2013-07-11 22:19:03 +09:00
takezoe
b6bcebc588 Fix activity message. 2013-07-11 22:13:53 +09:00
shimamoto
2f3aa57d23 Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-07-11 22:02:44 +09:00
shimamoto
a123774bab (refs #11) When the issue is closed or reopened, the comment id not
required.
2013-07-11 22:02:12 +09:00
takezoe
7f56b50267 Remove unnecessary code. 2013-07-11 21:26:27 +09:00
takezoe
386f0dc142 (refs #36)Handle unresolved revision string. 2013-07-11 21:24:09 +09:00
takezoe
bf90811cef Replace String#format() with string interpolation. 2013-07-11 20:22:45 +09:00
takezoe
72e2c6dca7 Replace String#format() with string interpolation. 2013-07-11 20:19:11 +09:00
takezoe
81fe467b20 Improve Git repository creation. 2013-07-11 19:47:48 +09:00
shimamoto
d59e358caa (refs #11) Add permission to html. 2013-07-11 15:03:57 +09:00
shimamoto
063170463f (refs #11) Add permission to html. 2013-07-11 14:01:09 +09:00
shimamoto
f8a9851bb3 Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-07-11 13:44:12 +09:00
shimamoto
b81a30ef12 (refs #11) Implemented the batch update. 2013-07-11 13:43:42 +09:00
takezoe
62fb968c9a Fix showing branch if specified branch, tag or id does not exist. 2013-07-11 13:07:50 +09:00
takezoe
88b8567d2b Hide branch pulldown at Tags tab. 2013-07-11 12:50:05 +09:00
takezoe
7e4a295ef0 (refs #28)Add avatar icon to the issue detail page. 2013-07-11 12:26:37 +09:00
takezoe
289ed85365 Fix height of avatar icon. 2013-07-11 12:06:06 +09:00
takezoe
07dd459f3c Add method for request cache to app.Context. 2013-07-11 11:20:56 +09:00
takezoe
f104fab593 Rename StringUtil#encrypt() to sha1(). 2013-07-11 11:09:30 +09:00
takezoe
0170f9b44a Replace implicit conversion with implicit class. 2013-07-11 11:03:59 +09:00
takezoe
585d96949b Fix typo. 2013-07-11 11:02:48 +09:00
takezoe
46b3807f21 Fix redirect path after sign in. 2013-07-11 04:08:06 +09:00
takezoe
5e8a73e29d Small fix. 2013-07-11 04:07:42 +09:00
takezoe
796a276b65 (refs #28)Look up Gravatar if user icon is not configured. 2013-07-11 03:51:50 +09:00
takezoe
072290e544 Fix header and sign-in form presentation. 2013-07-11 03:50:00 +09:00
takezoe
5d5b642fa9 Fix avatar image style. 2013-07-11 00:49:03 +09:00
takezoe
70761f4ac1 (refs #27)Display assigned user on issue list. 2013-07-10 21:09:21 +09:00
takezoe
9d61f73e22 Generalize some methods in AccountController and UserManagementController. 2013-07-10 20:44:28 +09:00
takezoe
7a3c61a8d0 Generalize some methods in AccountController and UserManagementController. 2013-07-10 20:38:24 +09:00
takezoe
96872d7d41 (refs #28)Upload avatar part is separated from account editing form. 2013-07-10 20:20:05 +09:00
takezoe
485d6131d5 (refs #28)Display avatar images in some places. 2013-07-10 19:57:59 +09:00
takezoe
4893e9a58a (refs #28)Remove debug code. 2013-07-10 18:26:09 +09:00
takezoe
79480c1d73 (refs #28)Remove unnecessary import statement. 2013-07-10 18:25:24 +09:00
takezoe
02c015574f (refs #28)Fix information message. 2013-07-10 18:24:46 +09:00
takezoe
653872df8e (refs #28)Add SessionCleanupListener. 2013-07-10 18:23:56 +09:00
takezoe
248079f041 (refs #28)Display avatar icon on the activity timeline. 2013-07-10 14:37:00 +09:00
takezoe
2da756692b (refs #28)Avatar image can be uploaded at the account editing page. 2013-07-10 14:15:56 +09:00
shimamoto
240a749b87 (refs #11) Add button for batch update. 2013-07-10 12:13:19 +09:00
takezoe
e4324258d3 (refs #28)Implementing avatar image uploading. 2013-07-10 11:34:36 +09:00
takezoe
2c33abe5d1 (refs #28)Implementing avatar image uploading. 2013-07-10 03:01:46 +09:00
takezoe
09ef1e0319 (refs #28)Add Dropzone.js for Ajax based file uploading. 2013-07-10 03:01:14 +09:00
takezoe
c091d96999 Adjust div.box-header style. 2013-07-10 00:20:23 +09:00
takezoe
1978061a06 Display the message after settings updating is completed. 2013-07-10 00:16:55 +09:00
takezoe
617370e822 Rename SettingsController to RepositorySettingsController. 2013-07-10 00:09:30 +09:00
takezoe
0ed6a96781 Display the message after settings updating is completed. 2013-07-09 21:33:46 +09:00
takezoe
b3c3bf51ba Small fix. 2013-07-09 21:29:29 +09:00
takezoe
0e187fe888 Display last 3 commits for push action in the activity timeline. 2013-07-09 20:04:48 +09:00
takezoe
43efcf3a99 Adjust error message positions of sign-in form. 2013-07-09 19:58:39 +09:00
takezoe
ebc858aed9 (refs #31)Make it possible to create empty repository. 2013-07-09 19:41:00 +09:00
takezoe
f94af86ff9 Merge remote-tracking branch 'origin/master' 2013-07-09 15:45:49 +09:00
takezoe
da1c58bac6 Remove commit log before repository. 2013-07-09 15:44:45 +09:00
takezoe
c1c136f6c0 Remove activities before repository. 2013-07-09 13:18:13 +09:00
Naoki Takezoe
8a18119b53 Update README.md for 1.2 release. 2013-07-09 11:18:38 +09:00
takezoe
777142b992 (refs #20)Ignore response.setCharacterEncoding(). 2013-07-09 02:02:28 +09:00
takezoe
daa54029ed (refs #20)Remove charset from Content-Type header. 2013-07-08 21:36:18 +09:00
shimamoto
136a654639 Improve mapping of custom column type. 2013-07-08 17:25:31 +09:00
takezoe
f13e2c0d71 Insert issue comment from commit message as 'commit' action. 2013-07-08 15:33:53 +09:00
takezoe
97101248a2 (refs #22)Fix constraint for adding collaborator. 2013-07-08 15:23:40 +09:00
takezoe
5150b4b1b6 Small fix about presentation. 2013-07-08 15:22:37 +09:00
takezoe
a6d2381a68 Small fix about presentation. 2013-07-08 15:13:51 +09:00
Naoki Takezoe
29161feb49 Update README.md 2013-07-08 01:44:08 +09:00
Naoki Takezoe
1a4a1c2ccb Update README.md 2013-07-08 01:42:38 +09:00
takezoe
96dac65e31 (refs #4)Add 'News Feed' to the index page. 2013-07-07 14:05:01 +09:00
takezoe
6005282d9f (refs #4)The base of activity timeline is completed. 2013-07-07 13:40:04 +09:00
takezoe
129020dbc4 (refs #4)Implementing activity recording for git push. 2013-07-07 03:50:11 +09:00
takezoe
54e0242030 (refs #4)Record wiki activity. 2013-07-07 01:24:08 +09:00
takezoe
0e57f4064f Ignore IDEA configuration files. 2013-07-07 01:23:14 +09:00
takezoe
e50c4528a6 (refs #4)Add issue close, reopen and comment activity. 2013-07-06 22:07:51 +09:00
takezoe
342810aa3a (refs #4)Record issue creation activity. 2013-07-06 20:14:49 +09:00
takezoe
f84078c7ca (refs #4)Add 'Public Activity' tab to the account information page. 2013-07-06 20:03:34 +09:00
takezoe
eba81a6065 Remove unnecessary code. 2013-07-06 19:01:03 +09:00
takezoe
427e9197d8 Fix presentation when there are no repositories. 2013-07-06 17:02:17 +09:00
takezoe
67d6cf37a5 Display the commit count at the repository viewer. 2013-07-06 16:49:06 +09:00
takezoe
e6451c7ede (refs #21)Allow multi-byte chars in label name. 2013-07-06 04:19:35 +09:00
takezoe
bcd88a1342 (refs #24)Add GitBucket version to the header. 2013-07-06 03:11:05 +09:00
takezoe
5ea250d89d Improve issue id detection in Markdown. 2013-07-05 23:25:09 +09:00
95 changed files with 3808 additions and 672 deletions

4
.gitignore vendored
View File

@@ -14,3 +14,7 @@ project/plugins/project/
.classpath .classpath
.project .project
.cache .cache
# IntelliJ specific
.idea/
.idea_modules/

202
LICENSE Normal file
View 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.

View File

@@ -1,7 +1,7 @@
GitBucket 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: 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) - Repository viewer (some advanced features are not implemented)
- Wiki - Wiki
- Issues - Issues
- Activity timeline
- User management (for Administrators) - User management (for Administrators)
Following features are not implemented, but we will make them in the future release! Following features are not implemented, but we will make them in the future release!
- Fork and pull request - Fork and pull request
- Timeline
- Search - Search
- Network graph - Network graph
- Statics - Statics
@@ -32,9 +32,29 @@ Installation
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
To upgrade GitBucket, only replace gitbucket.war.
Release Notes Release Notes
-------- --------
### 1.3 - xx 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 ### 1.0 - 04 Jul 2013
- This is a first public release. - This is a first public release.

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

View 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);

View 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';

View File

@@ -5,6 +5,7 @@ import javax.servlet._
class ScalatraBootstrap extends LifeCycle { class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) { override def init(context: ServletContext) {
context.mount(new IndexController, "/") context.mount(new IndexController, "/")
context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*") context.mount(new SignInController, "/*")
context.mount(new UserManagementController, "/*") context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")
@@ -15,7 +16,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new LabelsController, "/*") context.mount(new LabelsController, "/*")
context.mount(new MilestonesController, "/*") context.mount(new MilestonesController, "/*")
context.mount(new IssuesController, "/*") context.mount(new IssuesController, "/*")
context.mount(new SettingsController, "/*") context.mount(new RepositorySettingsController, "/*")
val dir = new java.io.File(_root_.util.Directory.GitBucketHome) val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
if(!dir.exists){ if(!dir.exists){

View File

@@ -1,31 +1,41 @@
package app package app
import service._ import service._
import util.OneselfAuthenticator import util.{FileUtil, FileUploadUtil, OneselfAuthenticator}
import util.StringUtil._ import util.StringUtil._
import util.Directory._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
class AccountController extends AccountControllerBase 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 { trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
self: SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator => 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( val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))) "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
)(AccountNewForm.apply) )(AccountNewForm.apply)
val editForm = mapping( val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))), "password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))) "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply) )(AccountEditForm.apply)
/** /**
@@ -33,52 +43,63 @@ trait AccountControllerBase extends ControllerBase {
*/ */
get("/:userName") { get("/:userName") {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { getAccountByUserName(userName).map { x =>
account.html.info(_, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName))) params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" => account.html.activity(x, getActivitiesByUser(userName, true))
// Repositories
case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
}
} getOrElse NotFound } 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 { get("/:userName/_edit")(oneselfOnly {
val userName = params("userName") 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 => post("/:userName/_edit", editForm)(oneselfOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
updateAccount(account.copy( updateAccount(account.copy(
password = form.password.map(encrypt).getOrElse(account.password), password = form.password.map(sha1).getOrElse(account.password),
mailAddress = form.mailAddress, mailAddress = form.mailAddress,
url = form.url)) 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 } getOrElse NotFound
}) })
get("/register"){ get("/register"){
if(loadSystemSettings().allowAccountRegistration){ if(loadSystemSettings().allowAccountRegistration){
account.html.edit(None) if(context.loginAccount.isDefined){
redirect("/")
} else {
account.html.edit(None, None)
}
} else NotFound } else NotFound
} }
post("/register", newForm){ newForm => post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){ 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") redirect("/signin")
} else NotFound } else NotFound
} }
// TODO Merge with UserManagementController
private def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
// TODO Merge with UserManagementController
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
} }

View File

@@ -1,14 +1,19 @@
package app package app
import model.Account import _root_.util.Directory._
import util.Validations import _root_.util.{FileUploadUtil, FileUtil, Validations}
import org.scalatra._ import org.scalatra._
import org.scalatra.json._ import org.scalatra.json._
import org.json4s._ import org.json4s._
import jp.sf.amateras.scalatra.forms._ 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.HttpServletRequest
/** /**
* Provides generic features for ScalatraServlet implementations. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase extends ScalatraFilter abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations { with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
@@ -18,7 +23,7 @@ abstract class ControllerBase extends ScalatraFilter
/** /**
* Returns the context object for the request. * 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 = { private def currentURL: String = {
val queryString = request.getQueryString val queryString = request.getQueryString
@@ -87,4 +92,60 @@ 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 { 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(FileUploadUtil.getUploadedFilename(fileId).get)
FileUtils.moveFile(
FileUploadUtil.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." }
}
}

View File

@@ -1,7 +1,7 @@
package app package app
import util.Directory._ import util.Directory._
import util.UsersAuthenticator import util.{JGitUtil, UsersAuthenticator}
import service._ import service._
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@@ -10,19 +10,23 @@ import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class CreateRepositoryController extends CreateRepositoryControllerBase 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. * Creates new repository.
*/ */
trait CreateRepositoryControllerBase extends ControllerBase { trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with WikiService with LabelsService with UsersAuthenticator => self: RepositoryService with WikiService with LabelsService with ActivityService
with UsersAuthenticator =>
case class RepositoryCreationForm(name: String, description: Option[String]) case class RepositoryCreationForm(name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
val form = mapping( val form = mapping(
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))), "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))) "description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply) )(RepositoryCreationForm.apply)
/** /**
@@ -36,10 +40,11 @@ trait CreateRepositoryControllerBase extends ControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", form)(usersOnly { form => post("/new", form)(usersOnly { form =>
val loginUserName = context.loginAccount.get.userName val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first // Insert to the database at first
createRepository(form.name, loginUserName, form.description) createRepository(form.name, loginUserName, form.description, form.isPrivate)
// Insert default labels // Insert default labels
createLabel(loginUserName, form.name, "bug", "fc2929") createLabel(loginUserName, form.name, "bug", "fc2929")
@@ -51,41 +56,44 @@ trait CreateRepositoryControllerBase extends ControllerBase {
// Create the actual repository // Create the actual repository
val gitdir = getRepositoryDir(loginUserName, form.name) val gitdir = getRepositoryDir(loginUserName, form.name)
val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build JGitUtil.initRepository(gitdir)
repository.create if(form.createReadme){
val tmpdir = getInitRepositoryDir(loginUserName, form.name)
try {
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
val config = repository.getConfig // Create README.md
config.setBoolean("http", null, "receivepack", true) FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
config.save 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) val git = Git.open(tmpdir)
try { git.add.addFilepattern("README.md").call
// Clone the repository git.commit.setMessage("Initial commit").call
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call git.push.call
// Create README.md } finally {
FileUtils.writeStringToFile(new File(tmpdir, "README.md"), FileUtils.deleteDirectory(tmpdir)
if(form.description.nonEmpty){ }
form.name + "\n===============\n\n" + form.description.get
} else {
form.name + "\n===============\n"
}, "UTF-8")
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit.setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
} }
// Create Wiki repository // Create Wiki repository
createWikiRepository(context.loginAccount.get, form.name) createWikiRepository(loginAccount, form.name)
// Record activity
recordCreateRepositoryActivity(loginUserName, form.name, loginUserName)
// redirect to the repository // redirect to the repository
redirect("/%s/%s".format(loginUserName, form.name)) redirect(s"/${loginUserName}/${form.name}")
}) })
/** /**

View File

@@ -0,0 +1,31 @@
package app
import util.{FileUtil, FileUploadUtil}
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 [[util.FileUploadUtil#getTemporaryFile()]] with this id.
*/
// TODO Remove temporary files at session timeout by session listener.
class FileUploadController extends ScalatraServlet with FileUploadSupport with FlashMapSupport {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => {
val fileId = FileUploadUtil.generateFileId
FileUtils.writeByteArrayToFile(FileUploadUtil.getTemporaryFile(fileId), file.get)
session += "upload_" + fileId -> file.name
Ok(fileId)
}
case None => BadRequest
}
}
}

View File

@@ -2,13 +2,20 @@ package app
import service._ import service._
class IndexController extends IndexControllerBase with RepositoryService with AccountService with SystemSettingsService class IndexController extends IndexControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService
trait IndexControllerBase extends ControllerBase { self: RepositoryService with SystemSettingsService => trait IndexControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService =>
get("/"){ get("/"){
html.index(getAccessibleRepositories(context.loginAccount, baseUrl), loadSystemSettings(), val loginAccount = context.loginAccount
context.loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil))
html.index(getRecentActivities(),
getAccessibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil)
)
} }
} }

View File

@@ -8,19 +8,18 @@ import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAut
import org.scalatra.Ok import org.scalatra.Ok
class IssuesController extends IssuesControllerBase 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 with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase { 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 => with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String]) case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
val issueCreateForm = mapping( val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
@@ -40,6 +39,11 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(label("Comment", text(required))) "content" -> trim(label("Comment", text(required)))
)(CommentForm.apply) )(CommentForm.apply)
val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _) searchIssues("all", _)
}) })
@@ -86,11 +90,14 @@ trait IssuesControllerBase extends ControllerBase {
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
val writable = hasWritePermission(owner, name, context.loginAccount) 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.assignedUserName else None,
if(writable) form.milestoneId else None) if(writable) form.milestoneId else None)
// insert labels
if(writable){ if(writable){
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) 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) => 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 => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditable(owner, name, issue.openedUserName)){
updateIssue(owner, name, issue.issueId, form.title, form.content) 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 } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner handleComment(form.issueId, Some(form.content), repository)() map { id =>
val name = repository.name redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
getIssue(owner, name, form.issueId.toString).map { issue => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
redirect("/%s/%s/issues/%d#comment-%d".format( handleComment(form.issueId, form.content, repository)() map { id =>
owner, name, form.issueId, redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
createComment(owner, name, context.loginAccount.get.userName, } getOrElse NotFound
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)
))
}
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => 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 => getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content) 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 } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
@@ -159,7 +162,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("title" -> x.title, Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
@@ -176,7 +179,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content, Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
@@ -198,25 +201,103 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
params.get("assignedUserName") filter (_.trim != ""))
Ok("updated") Ok("updated")
}) })
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
params.get("milestoneId") collect { case x if x.trim != "" => x.toInt })
Ok("updated") Ok("updated")
}) })
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 = private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName 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
*/
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) = { private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner val owner = repository.owner
val repoName = repository.name val repoName = repository.name
val userName = if(filter != "all") Some(params("userName")) else None val userName = if(filter != "all") Some(params("userName")) else None
val sessionKey = "%s/%s/issues".format(owner, repoName) val sessionKey = s"${owner}/${repoName}/issues"
val page = try { val page = try {
val i = params.getOrElse("page", "1").toInt val i = params.getOrElse("page", "1").toInt
@@ -235,8 +316,9 @@ trait IssuesControllerBase extends ControllerBase {
issues.html.list( issues.html.list(
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit), searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
page, page,
getLabels(owner, repoName), (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName).filter(_.closedDate.isEmpty), getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
getLabels(owner, repoName),
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName), countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName), countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
countIssue(owner, repoName, condition, "all", None), countIssue(owner, repoName, condition, "all", None),

View File

@@ -13,18 +13,18 @@ trait LabelsControllerBase extends ControllerBase {
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
val newForm = mapping( 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))) "newColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
val editForm = mapping( 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))) "editColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1)) 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 => 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) 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
}
}
} }

View File

@@ -35,7 +35,7 @@ trait MilestonesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate) 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 => 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) => post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name)) redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
closeMilestone(milestone) closeMilestone(milestone)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name)) redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
openMilestone(milestone) openMilestone(milestone)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name)) redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId) 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 } getOrElse NotFound
}) })

View File

@@ -5,11 +5,12 @@ import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator} import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils 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 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 => self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator =>
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean) case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
@@ -30,14 +31,14 @@ trait SettingsControllerBase extends ControllerBase {
* Redirect to the Options page. * Redirect to the Options page.
*/ */
get("/:owner/:repository/settings")(ownerOnly { repository => 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. * Display the Options page.
*/ */
get("/:owner/:repository/settings/options")(ownerOnly { get("/:owner/:repository/settings/options")(ownerOnly {
settings.html.options(_) settings.html.options(_, flash.get("info"))
}) })
/** /**
@@ -45,7 +46,8 @@ trait SettingsControllerBase extends ControllerBase {
*/ */
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate) 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")
}) })
/** /**
@@ -68,7 +70,7 @@ trait SettingsControllerBase extends ControllerBase {
*/ */
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
addCollaborator(repository.owner, repository.name, form.userName) addCollaborator(repository.owner, repository.name, form.userName)
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name)) redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
/** /**
@@ -76,7 +78,7 @@ trait SettingsControllerBase extends ControllerBase {
*/ */
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
removeCollaborator(repository.owner, repository.name, params("name")) removeCollaborator(repository.owner, repository.name, params("name"))
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name)) redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
/** /**
@@ -96,7 +98,7 @@ trait SettingsControllerBase extends ControllerBase {
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
redirect("/%s".format(repository.owner)) redirect(s"/${repository.owner}")
}) })
/** /**
@@ -104,17 +106,12 @@ trait SettingsControllerBase extends ControllerBase {
*/ */
private def collaborator: Constraint = new Constraint(){ private def collaborator: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = { def validate(name: String, value: String): Option[String] = {
val paths = request.getRequestURI.split("/")
getAccountByUserName(value) match { getAccountByUserName(value) match {
case None => Some("User does not exist.") 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) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName))
case Some(x) => { => Some("User can access this repository already.")
val paths = request.getRequestURI.split("/") case _ => None
if(getCollaborators(paths(1), paths(2)).contains(x.userName)){
Some("User can access this repository already.")
} else {
None
}
}
} }
} }
} }

View File

@@ -27,8 +27,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html" contentType = "text/html"
view.helpers.markdown(params("content"), repository, view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean, params("enableWikiLink").toBoolean,
params("enableCommitLink").toBoolean, params("enableRefsLink").toBoolean)
params("enableIssueLink").toBoolean)
}) })
/** /**
@@ -58,13 +57,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/commits/:branch")(referrersOnly { repository => get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
val branchName = params("branch") val branchName = params("branch")
val page = params.getOrElse("page", "1").toInt val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30) JGitUtil.getCommitLog(git, branchName, page, 30) match {
case Right((logs, hasNext)) =>
repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) => repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext) }, page, hasNext)
case Left(_) => NotFound
}
} }
}) })
@@ -77,12 +77,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val page = params.getOrElse("page", "1").toInt val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30, path) JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
repo.html.commits(path.split("/").toList, branchName, repository, repo.html.commits(path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext) }, page, hasNext)
case Left(_) => NotFound
}
} }
}) })
@@ -210,40 +212,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* @return HTML of the file list * @return HTML of the file list
*/ */
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
getRepository(repository.owner, repository.name, baseUrl).map { repositoryInfo => if(repository.commitCount == 0){
val revision = if(revstr.isEmpty){ repo.html.guide(repository)
repositoryInfo.repository.defaultBranch } else {
} else { JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
revstr 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
} }
} }

View File

@@ -25,7 +25,7 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
post("/signin", form){ form => post("/signin", form){ form =>
val account = getAccountByUserName(form.userName) val account = getAccountByUserName(form.userName)
if(account.isEmpty || account.get.password != encrypt(form.password)){ if(account.isEmpty || account.get.password != sha1(form.password)){
redirect("/signin") redirect("/signin")
} else { } else {
session.setAttribute("LOGIN_ACCOUNT", account.get) session.setAttribute("LOGIN_ACCOUNT", account.get)
@@ -35,7 +35,7 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
session.removeAttribute("REDIRECT") session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String]) redirect(redirectUrl.asInstanceOf[String])
}.getOrElse { }.getOrElse {
redirect("/%s".format(account.get.userName)) redirect("/")
} }
} }
} }

View File

@@ -4,11 +4,12 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator with SystemSettingsService with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase { trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
self: SystemSettingsService with AccountService with AdminAuthenticator => self: SystemSettingsService with AccountService with AdminAuthenticator =>
private case class SystemSettingsForm(allowAccountRegistration: Boolean) private case class SystemSettingsForm(allowAccountRegistration: Boolean)
@@ -19,11 +20,12 @@ trait SystemSettingsControllerBase extends ControllerBase {
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
admin.html.system(loadSystemSettings()) admin.html.system(loadSystemSettings(), flash.get("info"))
}) })
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(SystemSettings(form.allowAccountRegistration)) saveSystemSettings(SystemSettings(form.allowAccountRegistration))
flash += "info" -> "System settings has been updated."
redirect("/admin/system") redirect("/admin/system")
}) })

View File

@@ -1,23 +1,31 @@
package app package app
import service._ import service._
import util.AdminAuthenticator import util.{FileUploadUtil, FileUtil, AdminAuthenticator}
import util.StringUtil._ import util.StringUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import util.Directory._
import scala.Some
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
trait UserManagementControllerBase extends ControllerBase { self: AccountService with AdminAuthenticator => trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with AdminAuthenticator =>
case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]) case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean, url: Option[String]) url: Option[String], fileId: Option[String])
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())), "isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))) "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
)(UserNewForm.apply) )(UserNewForm.apply)
val editForm = mapping( val editForm = mapping(
@@ -25,7 +33,9 @@ trait UserManagementControllerBase extends ControllerBase { self: AccountService
"password" -> trim(label("Password" , optional(text(maxlength(20))))), "password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" , boolean())), "isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))) "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(UserEditForm.apply) )(UserEditForm.apply)
get("/admin/users")(adminOnly { get("/admin/users")(adminOnly {
@@ -37,7 +47,8 @@ trait UserManagementControllerBase extends ControllerBase { self: AccountService
}) })
post("/admin/users/_new", newForm)(adminOnly { form => post("/admin/users/_new", newForm)(adminOnly { form =>
createAccount(form.userName, encrypt(form.password), form.mailAddress, form.isAdmin, form.url) createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
@@ -50,27 +61,15 @@ trait UserManagementControllerBase extends ControllerBase { self: AccountService
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy( updateAccount(getAccountByUserName(userName).get.copy(
password = form.password.map(encrypt).getOrElse(account.password), password = form.password.map(sha1).getOrElse(account.password),
mailAddress = form.mailAddress, mailAddress = form.mailAddress,
isAdmin = form.isAdmin, isAdmin = form.isAdmin,
url = form.url)) url = form.url))
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
} getOrElse NotFound } getOrElse NotFound
}) })
// TODO Merge with AccountController?
private def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
// TODO Merge with AccountController?
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
} }

View File

@@ -1,27 +1,29 @@
package app package app
import service._ import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil} import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
import util.Directory._ import util.Directory._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class WikiController extends WikiControllerBase 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 { 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) case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
val newForm = mapping( 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))), "content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text())) "currentPageName" -> trim(label("Current page name" , text()))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
val editForm = mapping( 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))), "content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required))) "currentPageName" -> trim(label("Current page name" , text(required)))
@@ -30,27 +32,30 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page => getWikiPage(repository.owner, repository.name, "Home").map { page =>
wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) 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 => 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 => getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) 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 => 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 => 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 => get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = params("page") val pageName = StringUtil.urlDecode(params("page"))
val commitId = params("commitId").split("\\.\\.\\.") val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
@@ -67,16 +72,20 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository => get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = params("page") val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
}) })
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, 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) 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 { get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
@@ -84,20 +93,24 @@ trait WikiControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, context.loginAccount.get, form.message.getOrElse("")) form.content, context.loginAccount.get, form.message.getOrElse(""))
updateLastActivityDate(repository.owner, repository.name) 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 => 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) 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 => get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
@@ -107,7 +120,10 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => 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.") 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
}
}
} }

View File

@@ -2,7 +2,7 @@ package model
import scala.slick.driver.H2Driver.simple._ 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 userName = column[String]("USER_NAME", O PrimaryKey)
def mailAddress = column[String]("MAIL_ADDRESS") def mailAddress = column[String]("MAIL_ADDRESS")
def password = column[String]("PASSWORD") def password = column[String]("PASSWORD")
@@ -11,7 +11,8 @@ object Accounts extends Table[Account]("ACCOUNT") with Functions {
def registeredDate = column[java.util.Date]("REGISTERED_DATE") def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE") def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_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 * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? <> (Account, Account.unapply _)
} }
case class Account( case class Account(
@@ -22,5 +23,6 @@ case class Account(
url: Option[String], url: Option[String],
registeredDate: java.util.Date, registeredDate: java.util.Date,
updatedDate: java.util.Date, updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date] lastLoginDate: Option[java.util.Date],
image: Option[String]
) )

View 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
)

View File

@@ -7,7 +7,7 @@ object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTempla
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
} }
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate with Functions { object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
def openedUserName = column[String]("OPENED_USER_NAME") def openedUserName = column[String]("OPENED_USER_NAME")
def assignedUserName = column[String]("ASSIGNED_USER_NAME") def assignedUserName = column[String]("ASSIGNED_USER_NAME")
def title = column[String]("TITLE") def title = column[String]("TITLE")

View File

@@ -2,16 +2,16 @@ package model
import scala.slick.driver.H2Driver.simple._ 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 commentId = column[Int]("COMMENT_ID", O AutoInc)
def action = column[String]("ACTION") def action = column[String]("ACTION")
def commentedUserName = column[String]("COMMENTED_USER_NAME") def commentedUserName = column[String]("COMMENTED_USER_NAME")
def content = column[String]("CONTENT") def content = column[String]("CONTENT")
def registeredDate = column[java.util.Date]("REGISTERED_DATE") def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_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 def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
} }
@@ -20,7 +20,7 @@ case class IssueComment(
repositoryName: String, repositoryName: String,
issueId: Int, issueId: Int,
commentId: Int, commentId: Int,
action: Option[String], action: String,
commentedUserName: String, commentedUserName: String,
content: String, content: String,
registeredDate: java.util.Date, registeredDate: java.util.Date,

View File

@@ -2,7 +2,7 @@ package model
import scala.slick.driver.H2Driver.simple._ 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 title = column[String]("TITLE")
def description = column[String]("DESCRIPTION") def description = column[String]("DESCRIPTION")
def dueDate = column[java.util.Date]("DUE_DATE") def dueDate = column[java.util.Date]("DUE_DATE")

View File

@@ -2,7 +2,7 @@ package model
import scala.slick.driver.H2Driver.simple._ 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 isPrivate = column[Boolean]("PRIVATE")
def description = column[String]("DESCRIPTION") def description = column[String]("DESCRIPTION")
def defaultBranch = column[String]("DEFAULT_BRANCH") def defaultBranch = column[String]("DEFAULT_BRANCH")

View File

@@ -1,8 +1,6 @@
package model package object model {
import scala.slick.lifted.MappedTypeMapper
import scala.slick.lifted.MappedTypeMapper
protected[model] trait Functions {
// java.util.Date TypeMapper // java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp]( implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime), d => new java.sql.Timestamp(d.getTime),

View File

@@ -1,7 +1,6 @@
package service package service
import model._ import model._
import Accounts._
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
@@ -24,7 +23,8 @@ trait AccountService {
url = url, url = url,
registeredDate = currentDate, registeredDate = currentDate,
updatedDate = currentDate, updatedDate = currentDate,
lastLoginDate = None) lastLoginDate = None,
image = None)
def updateAccount(account: Account): Unit = def updateAccount(account: Account): Unit =
Accounts Accounts
@@ -38,7 +38,10 @@ trait AccountService {
account.registeredDate, account.registeredDate,
currentDate, currentDate,
account.lastLoginDate) 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 = def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate) Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)

View 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
}

View File

@@ -6,7 +6,7 @@ import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation import Q.interpolation
import model._ import model._
import Issues._ import util.StringUtil._
import util.Implicits._ import util.Implicits._
trait IssuesService { trait IssuesService {
@@ -36,6 +36,9 @@ trait IssuesService {
.map ( _._2 ) .map ( _._2 )
.list .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. * Returns the count of the search result against issues.
* *
@@ -99,7 +102,10 @@ trait IssuesService {
// get issues and comment count // get issues and comment count
val issues = searchIssueQuery(owner, repository, condition, filter, userName) val issues = searchIssueQuery(owner, repository, condition, filter, userName)
.leftJoin(Query(IssueComments) .leftJoin(Query(IssueComments)
.filter { _.byRepository(owner, repository) } .filter { t =>
(t.byRepository(owner, repository)) &&
(t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
}
.groupBy { _.issueId } .groupBy { _.issueId }
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1) .map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
.sortBy { case (t1, t2) => .sortBy { case (t1, t2) =>
@@ -189,7 +195,7 @@ trait IssuesService {
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def createComment(owner: String, repository: String, loginUser: String, def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: Option[String]) = issueId: Int, content: String, action: String) =
IssueComments.autoInc insert ( IssueComments.autoInc insert (
owner, owner,
repository, repository,
@@ -234,7 +240,6 @@ trait IssuesService {
} }
object IssuesService { object IssuesService {
import java.net.URLEncoder
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
val IssueLimit = 30 val IssueLimit = 30
@@ -246,8 +251,6 @@ object IssuesService {
sort: String = "created", sort: String = "created",
direction: String = "desc"){ direction: String = "desc"){
import IssueSearchCondition._
def toURL: String = def toURL: String =
"?" + List( "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
@@ -263,8 +266,6 @@ object IssuesService {
object IssueSearchCondition { 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] = { private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
val value = request.getParameter(name) val value = request.getParameter(name)
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)

View File

@@ -4,7 +4,6 @@ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
import model._ import model._
import Milestones._
trait MilestonesService { trait MilestonesService {

View File

@@ -1,7 +1,6 @@
package service package service
import model._ import model._
import Repositories._
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
import util.JGitUtil import util.JGitUtil
@@ -12,19 +11,17 @@ trait RepositoryService { self: AccountService =>
/** /**
* Creates a new repository. * 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 repositoryName the repository name
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param description the repository description * @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 Repositories insert
Repository( Repository(
userName = userName, userName = userName,
repositoryName = repositoryName, repositoryName = repositoryName,
isPrivate = false, isPrivate = isPrivate,
description = description, description = description,
defaultBranch = "master", defaultBranch = "master",
registeredDate = currentDate, registeredDate = currentDate,
@@ -35,6 +32,8 @@ trait RepositoryService { self: AccountService =>
} }
def deleteRepository(userName: String, repositoryName: String): Unit = { 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 Collaborators .filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete Labels .filter(_.byRepository(userName, repositoryName)).delete
@@ -83,8 +82,7 @@ trait RepositoryService { self: AccountService =>
} }
q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository => q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
} }
} }
@@ -98,8 +96,7 @@ trait RepositoryService { self: AccountService =>
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
} }
} }
@@ -112,9 +109,8 @@ trait RepositoryService { self: AccountService =>
*/ */
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = { def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = {
def createRepositoryInfo(repository: Repository): RepositoryInfo = { def newRepositoryInfo(repository: Repository): RepositoryInfo = {
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
} }
(account match { (account match {
@@ -127,7 +123,7 @@ trait RepositoryService { self: AccountService =>
} }
// for Guests // for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind) case None => Query(Repositories) filter(_.isPrivate is false.bind)
}).sortBy(_.lastActivityDate desc).list.map(createRepositoryInfo _) }).sortBy(_.lastActivityDate desc).list.map(newRepositoryInfo _)
} }
/** /**
@@ -189,6 +185,12 @@ trait RepositoryService { self: AccountService =>
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, 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)
}
}
} }

View 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)
}
}
}

View File

@@ -68,12 +68,10 @@ trait WikiService {
lock(owner.userName, repository){ lock(owner.userName, repository){
val dir = Directory.getWikiRepositoryDir(owner.userName, repository) val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
if(!dir.exists){ if(!dir.exists){
val repo = new RepositoryBuilder().setGitDir(dir).setBare.build
try { try {
repo.create JGitUtil.initRepository(dir)
saveWikiPage(owner.userName, repository, "Home", "Home", "Welcome to the %s wiki!!".format(repository), owner, "Initial Commit") saveWikiPage(owner.userName, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", owner, "Initial Commit")
} finally { } finally {
repo.close
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge' // 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.userName, repository))
} }

View File

@@ -25,7 +25,7 @@ object AutoUpdate {
* 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 = { 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) val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
if(in != null){ if(in != null){
val sql = IOUtils.toString(in, "UTF-8") val sql = IOUtils.toString(in, "UTF-8")
@@ -42,15 +42,33 @@ 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. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
Version(1, 1), new Version(1, 3){
Version(1, 0) 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)
) )
/** /**

View File

@@ -21,6 +21,10 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
val request = req.asInstanceOf[HttpServletRequest] val request = req.asInstanceOf[HttpServletRequest]
val response = res.asInstanceOf[HttpServletResponse] val response = res.asInstanceOf[HttpServletResponse]
val wrappedResponse = new HttpServletResponseWrapper(response){
override def setCharacterEncoding(encoding: String) = {}
}
try { try {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
val repositoryOwner = paths(2) val repositoryOwner = paths(2)
@@ -29,14 +33,14 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match { getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
case Some(repository) => { case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){ if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){
chain.doFilter(req, res) chain.doFilter(req, wrappedResponse)
} else { } else {
request.getHeader("Authorization") match { request.getHeader("Authorization") match {
case null => requireAuth(response) case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match { case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) if(isWritableUser(username, password, repository)) => { case Array(username, password) if(isWritableUser(username, password, repository)) => {
request.setAttribute("USER_NAME", username) request.setAttribute("USER_NAME", username)
chain.doFilter(req, res) chain.doFilter(req, wrappedResponse)
} }
case _ => requireAuth(response) case _ => requireAuth(response)
} }
@@ -55,7 +59,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = { private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = {
getAccountByUserName(username).map { account => 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 } getOrElse false
} }

View File

@@ -49,7 +49,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db) 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("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName) logger.debug("userName:" + userName)
@@ -60,27 +60,52 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository)) receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName))
receivePack receivePack
} }
} }
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String) extends PostReceiveHook class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService { with RepositoryService with AccountService with IssuesService with ActivityService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git => JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git =>
commands.asScala.foreach { command => commands.asScala.foreach { command =>
JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name).foreach { commit => val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData => val refName = command.getRefName.split("/")
val issueId = matchData.group(2)
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){ // apply issue comment
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, None) 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 _ =>
} }
} }
} }

View File

@@ -0,0 +1,17 @@
package servlet
import util.FileUploadUtil
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
/**
* Removes session associated temporary files when session is destroyed.
*/
class SessionCleanupListener extends HttpSessionListener {
def sessionCreated(se: HttpSessionEvent): Unit = {}
def sessionDestroyed(se: HttpSessionEvent): Unit = {
FileUploadUtil.removeTemporaryFiles()(se.getSession)
}
}

View File

@@ -6,7 +6,7 @@ import javax.servlet.http.HttpServletRequest
import scala.slick.session.Database 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 { class TransactionFilter extends Filter {
@@ -21,7 +21,6 @@ class TransactionFilter extends Filter {
// assets don't need transaction // assets don't need transaction
chain.doFilter(req, res) chain.doFilter(req, res)
} else { } else {
// TODO begin transaction!
val context = req.getServletContext val context = req.getServletContext
Database.forURL(context.getInitParameter("db.url"), Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"), context.getInitParameter("db.user"),

View File

@@ -13,13 +13,13 @@ object Directory {
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = "%s/repositories".format(GitBucketHome) val RepositoryHome = s"${GitBucketHome}/repositories"
/** /**
* Repository names of the specified user. * Repository names of the specified user.
*/ */
def getRepositories(owner: String): List[String] = { 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){ if(dir.exists){
dir.listFiles.filter { file => dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git") file.isDirectory && !file.getName.endsWith(".wiki.git")
@@ -33,19 +33,24 @@ object Directory {
* Substance directory of the repository. * Substance directory of the repository.
*/ */
def getRepositoryDir(owner: String, repository: String): File = 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. * Root of temporary directories for the specified repository.
*/ */
def getTemporaryDir(owner: String, repository: String): File = 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. * Temporary directory which is used to create an archive to download repository contents.
*/ */
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File = 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. * Temporary directory which is used in the repository creation.
@@ -60,7 +65,7 @@ object Directory {
* Substance directory of the wiki repository. * Substance directory of the wiki repository.
*/ */
def getWikiRepositoryDir(owner: String, repository: String): File = 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. * Wiki working directory which is cloned from the wiki repository.

View File

@@ -0,0 +1,33 @@
package util
import java.text.SimpleDateFormat
import javax.servlet.http.HttpSession
import util.Directory._
import org.apache.commons.io.FileUtils
object FileUploadUtil {
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
}
}

View File

@@ -44,4 +44,13 @@ object FileUtil {
} }
} }
def getExtension(name: String): String = {
val index = name.lastIndexOf('.')
if(index >= 0){
name.substring(index + 1)
} else {
""
}
}
} }

View File

@@ -1,14 +1,14 @@
package util package util
import twirl.api.Html
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex
/** /**
* Provides some usable implicit conversions. * Provides some usable implicit conversions.
*/ */
object Implicits { 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) def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@@ -26,8 +26,27 @@ object Implicits {
} }
// TODO Should this implicit conversion move to model.Functions? // TODO Should this implicit conversion move to model.Functions?
implicit def extendsColumn(c1: Column[Boolean]) = new { implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1 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
}
}
} }

View File

@@ -14,6 +14,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.util.io.DisabledOutputStream import org.eclipse.jgit.util.io.DisabledOutputStream
import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
@@ -26,10 +27,11 @@ object JGitUtil {
* @param owner the user name of the repository owner * @param owner the user name of the repository owner
* @param name the repository name * @param name the repository name
* @param url the repository URL * @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 branchList the list of branch names
* @param tags the list of tags * @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. * The file data for the file list of the repository viewer.
@@ -50,14 +52,21 @@ object JGitUtil {
* @param id the commit id * @param id the commit id
* @param time the commit time * @param time the commit time
* @param committer the committer name * @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message * @param shortMessage the short message
* @param fullMessage the full message * @param fullMessage the full message
* @param parents the list of parent commit id * @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( 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) rev.getParents().map(_.name).toList)
val summary = { val summary = {
@@ -141,18 +150,35 @@ object JGitUtil {
*/ */
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
withGit(getRepositoryDir(owner, repository)){ git => withGit(getRepositoryDir(owner, repository)){ git =>
RepositoryInfo( try {
owner, repository, baseUrl + "/git/%s/%s.git".format(owner, repository), // get commit count
// branches val i = git.log.all.call.iterator
git.branchList.call.asScala.map { ref => var commitCount = 0
ref.getName.replaceFirst("^refs/heads/", "") while(i.hasNext && commitCount <= 1000){
}.toList, i.next
// tags commitCount = commitCount + 1
git.tagList.call.asScala.map { ref => }
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName) RepositoryInfo(
}.toList 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. * @param path filters by this path. default is no filter.
* @return a tuple of the commit list and whether has next * @return a tuple of the commit list and whether has next
*/ */
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 val fixedPage = if(page <= 0) 1 else page
@scala.annotation.tailrec @scala.annotation.tailrec
@@ -248,20 +274,25 @@ object JGitUtil {
} }
val revWalk = new RevWalk(git.getRepository) val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision))) val objectId = git.getRepository.resolve(revision)
if(path.nonEmpty){ if(objectId == null){
revWalk.setRevFilter(new RevFilter(){ Left(s"${revision} can't be resolved.")
def include(walk: RevWalk, commit: RevCommit): Boolean = { } else {
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty revWalk.markStart(revWalk.parseCommit(objectId))
} if(path.nonEmpty){
override def clone(): RevFilter = this 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
} }
/** /**
@@ -483,4 +514,20 @@ 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
}
}
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
}
} }

View File

@@ -1,11 +1,23 @@
package util package util
import java.net.{URLDecoder, URLEncoder}
object StringUtil { object StringUtil {
def encrypt(value: String): String = { def sha1(value: String): String = {
val md = java.security.MessageDigest.getInstance("SHA-1") val md = java.security.MessageDigest.getInstance("SHA-1")
md.update(value.getBytes) md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString 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")
} }

View File

@@ -11,9 +11,9 @@ trait Validations {
def identifier: Constraint = new Constraint(){ def identifier: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = def validate(name: String, value: String): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){ 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("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
Some("%s starts with invalid character.".format(name)) Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }

View File

@@ -0,0 +1,31 @@
package view
import service.RequestCache
import twirl.api.Html
import util.StringUtil
trait AvatarImageProvider { self: RequestCache =>
/**
* Returns &lt;img&gt; 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).collect { case account if(account.image.isEmpty) =>
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
} 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;" />""")
}
}
}

View 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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
// 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""")
}
}

View File

@@ -1,10 +1,12 @@
package view package view
import util.StringUtil
import org.parboiled.common.StringUtils import org.parboiled.common.StringUtils
import org.pegdown._ import org.pegdown._
import org.pegdown.ast._ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.RequestCache
object Markdown { object Markdown {
@@ -12,12 +14,17 @@ object Markdown {
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, 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( val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES
).parseMarkdown(markdown.toCharArray) ).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, context, repository, enableWikiLink, enableCommitLink, enableIssueLink).toHtml(rootNode) new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
} }
} }
@@ -33,11 +40,10 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} else { } else {
(text, text) (text, text)
} }
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
"/wiki/" + java.net.URLEncoder.encode(page.replace(' ', '-'), "UTF-8")
new Rendering(url, label) new Rendering(url, label)
} catch { } catch {
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException(); case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
} }
} else { } else {
super.render(node) super.render(node)
@@ -64,24 +70,13 @@ class GitBucketVerbatimSerializer extends VerbatimSerializer {
class GitBucketHtmlSerializer( class GitBucketHtmlSerializer(
markdown: String, markdown: String,
context: app.Context,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableWikiLink: Boolean,
enableCommitLink: Boolean, enableRefsLink: Boolean
enableIssueLink: Boolean )(implicit val context: app.Context) extends ToHtmlSerializer(
) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink), new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) { ) with LinkConverter with RequestCache {
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
}
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
printer.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/>") printer.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/>")
@@ -108,30 +103,13 @@ class GitBucketHtmlSerializer(
} }
override def visit(node: TextNode) { override def visit(node: TextNode) {
// convert commit id to link. // convert commit id and username to link.
val text1 = if(enableCommitLink) node.getText.replaceAll("(^|\\W)([0-9a-f]{40})(\\W|$)", val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
"<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
if (abbreviations.isEmpty) { if (abbreviations.isEmpty) {
printer.print(text2) printer.print(text)
} else { } else {
printWithAbbreviations(text2) printWithAbbreviations(text)
}
}
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)
} }
} }

View File

@@ -2,11 +2,13 @@ package view
import java.util.Date import java.util.Date
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import twirl.api.Html import twirl.api.Html
import util.StringUtil
import service.RequestCache
/** /**
* Provides helper methods for Twirl templates. * 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". * Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
@@ -29,39 +31,62 @@ object helpers {
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): Html = { enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
} }
/**
* Returns &lt;img&gt; 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. * Generates the url to the repository.
*/ */
def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String = 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. * 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. * 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"
/** /**
* 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 = implicit class RichHtmlSeq(seq: Seq[Html]) {
Html(value
// escape HTML tags
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
// 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 {
def mkHtml(separator: String) = Html(seq.mkString(separator)) def mkHtml(separator: String) = Html(seq.mkString(separator))
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))
} }

View File

@@ -0,0 +1,23 @@
@(account: model.Account, activities: List[model.Activity])(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">
<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">
@tab(account, "activity")
@helper.html.activities(activities)
</div>
</div>
</div>
}

View File

@@ -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 context._
@import view.helpers._ @import view.helpers._
@html.main((if(account.isDefined) "Edit your profile" else "Create your account")){ @html.main((if(account.isDefined) "Edit your profile" else "Create your account")){
@@ -7,34 +7,45 @@
} else { } else {
<h3>Create your account</h3> <h3>Create your account</h3>
} }
@helper.html.information(info)
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true"> <form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true">
@if(account.isEmpty){ <div class="row-fluid">
<fieldset> <div class="span6">
<label for="userName"><strong>User name</strong></label> @if(account.isEmpty){
<input type="text" name="userName" id="userName" value=""/> <fieldset>
<span id="error-userName" class="error"></span> <label for="userName"><strong>User name</strong></label>
</fieldset> <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)
} }
</label> <fieldset>
<input type="password" name="password" id="password" value=""/> <label for="password"><strong>Password</strong>
<span id="error-password" class="error"></span> @if(account.nonEmpty){
</fieldset> (Input to change password)
<fieldset> }
<label for="mailAddress"><strong>Mail Address</strong></label> </label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/> <input type="password" name="password" id="password" value=""/>
<span id="error-mailAddress" class="error"></span> <span id="error-password" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="url"><strong>URL (Optional)</strong></label> <label for="mailAddress"><strong>Mail Address</strong></label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/> <input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-url" class="error"></span> <span id="error-mailAddress" class="error"></span>
</fieldset> </fieldset>
<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){ @if(account.isDefined){
<input type="submit" class="btn btn-success" value="Save"/> <input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.get.userName)" class="btn">Cancel</a> <a href="@url(account.get.userName)" class="btn">Cancel</a>

View File

@@ -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>
}

View File

@@ -0,0 +1,42 @@
@(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="account-image">@avatar(account.userName, 200)</div>
<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">
@tab(account, "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>
}
}
</div>
</div>
</div>
}

View File

@@ -0,0 +1,14 @@
@(account: model.Account, active: String)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
<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>

View File

@@ -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 context._
@import view.helpers._ @import view.helpers._
@html.main("System Settings"){ @html.main("System Settings"){
@menu("system"){ @menu("system"){
@helper.html.information(info)
<form action="@path/admin/system" method="POST" validate="true"> <form action="@path/admin/system" method="POST" validate="true">
<div class="box"> <div class="box">
<div class="box-header">System Settings</div> <div class="box-header">System Settings</div>

View File

@@ -3,40 +3,50 @@
@html.main(if(account.isEmpty) "New User" else "Update User"){ @html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){ @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"> <form method="POST" action="@if(account.isEmpty){@path/admin/users/_new} else {@path/admin/users/@account.get.userName/_edit}" validate="true">
<fieldset> <div class="row-fluid">
<label for="userName"><strong>Username</strong></label> <div class="span6">
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/> <fieldset>
<span id="error-userName" class="error"></span> <label for="userName"><strong>Username</strong></label>
</fieldset> <input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<fieldset> <span id="error-userName" class="error"></span>
<label for="password"><strong>Password</strong> </fieldset>
@if(account.isDefined){ <fieldset>
(Input to change password) <label for="password"><strong>Password</strong>
} @if(account.isDefined){
</label> (Input to change password)
<input type="password" name="password" id="password" value="" autocomplete="off"/> }
<span id="error-password" class="error"></span> </label>
</fieldset> <input type="password" name="password" id="password" value="" autocomplete="off"/>
<fieldset> <span id="error-password" class="error"></span>
<label for="mailAddress"><strong>Mail Address</strong></label> </fieldset>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/> <fieldset>
<span id="error-mailAddress" class="error"></span> <label for="mailAddress"><strong>Mail Address</strong></label>
</fieldset> <input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<fieldset> <span id="error-mailAddress" class="error"></span>
<label><strong>User Type</strong></label> </fieldset>
<label for="userType_Normal"> <fieldset>
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal <label><strong>User Type</strong></label>
</label> <label for="userType_Normal">
<label for="userType_Admin"> <input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator </label>
</label> <label for="userType_Admin">
</fieldset> <input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
<fieldset> </label>
<label><strong>URL (Optional)</strong></label> </fieldset>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/> <fieldset>
<span id="error-url" class="error"></span> <label><strong>URL (Optional)</strong></label>
</fieldset> <input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<fieldset> <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">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/> <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> <a href="@path/admin/users" class="btn">Cancel</a>
</fieldset> </fieldset>

View File

@@ -14,6 +14,7 @@
<a href="@path/admin/users/@account.userName/_edit">Edit</a> <a href="@path/admin/users/@account.userName/_edit">Edit</a>
</div> </div>
<div class="strong"> <div class="strong">
@avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a> <a href="@url(account.userName)">@account.userName</a>
@if(account.isAdmin){ @if(account.isAdmin){
(Administrator) (Administrator)

View File

@@ -0,0 +1,43 @@
@(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 =>
@(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>
}
}

View File

@@ -1,7 +1,11 @@
@(body: Html) @(buttonValue: String = "")(body: Html)
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown"> <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> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">

View 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>
}

View File

@@ -1,5 +1,5 @@
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, @(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
enableCommitLink: Boolean, enableIssueLink: Boolean, style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context) style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="tabbable"> <div class="tabbable">
@@ -30,10 +30,9 @@ $(function(){
$('#preview').click(function(){ $('#preview').click(function(){
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...'); $('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', { $.post('@url(repository)/_preview', {
content : $('#content').val(), content : $('#content').val(),
enableWikiLink : @enableWikiLink, enableWikiLink : @enableWikiLink,
enableCommitLink : @enableCommitLink, enableRefsLink : @enableRefsLink
enableIssueLink : @enableIssueLink
}, function(data){ }, function(data){
$('#preview-area').html(data); $('#preview-area').html(data);
prettyPrint(); prettyPrint();

View 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>

View File

@@ -1,27 +1,14 @@
@(repositories: List[service.RepositoryService.RepositoryInfo], systemSettings: service.SystemSettingsService.SystemSettings, @(activities: List[model.Activity],
repositories: List[service.RepositoryService.RepositoryInfo],
systemSettings: service.SystemSettingsService.SystemSettings,
userRepositories: List[String])(implicit context: app.Context) userRepositories: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main("GitBucket"){ @main("GitBucket"){
<div class="row-fluid"> <div class="row-fluid">
<div class="span8"> <div class="span8">
<h3>Recent updated repositories</h3> <h3>News Feed</h3>
@repositories.map { repository => @helper.html.activities(activities)
<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 class="span4"> <div class="span4">
@if(loginAccount.isEmpty){ @if(loginAccount.isEmpty){
@@ -29,15 +16,47 @@
} else { } else {
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <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> </tr>
@userRepositories.map { repositoryName => @if(userRepositories.isEmpty){
<tr> <tr>
<td><a href="@path/@loginAccount.get.userName/@repositoryName">@repositoryName</a></td> <td>No repositories</td>
</tr> </tr>
} else {
@userRepositories.map { repositoryName =>
<tr>
<td><a href="@path/@loginAccount.get.userName/@repositoryName"><strong>@repositoryName</strong></a></td>
</tr>
}
} }
</table> </table>
} }
<table class="table table-bordered">
<tr>
<th class="metal">
Recent updated repositories
</th>
</tr>
@if(repositories.isEmpty){
<tr>
<td>No repositories</td>
</tr>
} else {
@repositories.map { repository =>
<tr>
<td>
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
</td>
</tr>
}
}
</table>
</div> </div>
</div> </div>

View File

@@ -11,19 +11,20 @@
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span9"> <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"> <div class="box-content">
<span id="error-title" class="error"></span> <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> <div>
<span id="label-assigned">No one is assigned</span> <span id="label-assigned">No one is assigned</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="assignedUserName" value=""/> <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><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li> <li class="divider"></li>
@collaborators.map { collaborator => @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,7 +32,7 @@
<span id="label-milestone">No milestone</span> <span id="label-milestone">No milestone</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/> <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><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li> <li class="divider"></li>
@milestones.map { milestone => @milestones.map { milestone =>
@@ -42,10 +43,12 @@
</div> </div>
</div> </div>
<hr> <hr>
@helper.html.preview(repository, "", false, true, true, "width: 650px; height: 200px;") @helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
</div> </div>
</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>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){

View File

@@ -1,7 +1,7 @@
@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context) @(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context)
@import context._ @import context._
<span id="error-edit-content-@commentId" class="error"></span> <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"/> <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> <span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
<script> <script>

View File

@@ -1,8 +1,8 @@
@(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context) @(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
@import context._ @import context._
<span id="error-edit-title" class="error"></span> <span id="error-edit-title" class="error"></span>
<input type="text" style="width: 730px;" id="edit-title" value="@title"/> <input type="text" style="width: 680px;" id="edit-title" value="@title"/>
<textarea style="width: 730px; height: 100px;" id="edit-content">@content.getOrElse("")</textarea> <textarea style="width: 680px; height: 100px;" id="edit-content">@content.getOrElse("")</textarea>
<input type="button" class="btn btn-small" value="Update Issue"/> <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> <span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
<script> <script>

View File

@@ -8,7 +8,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @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}"){
@html.header("issues", repository) @html.header("issues", repository)
@tab("issues", repository) @tab("issues", repository)
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
@@ -17,7 +17,8 @@
</ul> </ul>
<div class="row-fluid"> <div class="row-fluid">
<div class="span10"> <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="box-content" style="padding: 0px;">
<div class="issue-header"> <div class="issue-header">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
@@ -31,15 +32,15 @@
<div class="issue-info"> <div class="issue-info">
<span id="label-assigned"> <span id="label-assigned">
@issue.assignedUserName.map { userName => @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") }.getOrElse("No one is assigned")
</span> </span>
@if(hasWritePermission){ @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><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li> <li class="divider"></li>
@collaborators.map { collaborator => @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>
} }
} }
} }
@@ -52,7 +53,7 @@
}.getOrElse("No milestone") }.getOrElse("No milestone")
</span> </span>
@if(hasWritePermission){ @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><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li> <li class="divider"></li>
@milestones.map { milestone => @milestones.map { milestone =>
@@ -63,50 +64,57 @@
</div> </div>
</div> </div>
<div class="issue-content" id="issueContent"> <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>
</div> </div>
@comments.map { comment => @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 issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small"> <div class="box-header-small">
<i class="icon-comment"></i> <i class="icon-comment"></i>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented <a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented
<span class="pull-right"> <span class="pull-right">
@datetime(comment.registeredDate) @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> <a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
} }
</span> </span>
</div> </div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId"> <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>
</div> </div>
@comment.action.map { action => }
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="small issue-comment-action"> <div class="small issue-comment-action">
@if(action == "close"){ <span class="label label-important">Closed</span>
<span class="label label-important">Closed</span> <a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate) </div>
} else { }
<span class="label label-success">Reopened</span> @if(comment.action == "reopen" || comment.action == "reopen_comment"){
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate) <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> </div>
} }
} }
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<form action="@url(repository)/issue_comments/new" method="POST" validate="true"> <form method="POST" validate="true">
<div class="box"> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">
<div class="box-content"> <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>
</div> </div>
<input type="hidden" name="issueId" value="@issue.issueId"/> <div class="pull-right">
<input type="submit" class="btn btn-success" value="Comment"/> <input type="hidden" name="issueId" value="@issue.issueId"/>
@if(hasWritePermission || issue.openedUserName == loginAccount.get.userName){ <input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
<input type="submit" class="btn" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/> @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> </form>
} }
</div> </div>
@@ -117,14 +125,16 @@
<span class="label label-success issue-status">Open</span> <span class="label label-success issue-status">Open</span>
} }
<div class="small" style="text-align: center;"> <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> </div>
<hr/> <hr/>
<div style="margin-bottom: 8px;"> <div style="margin-bottom: 8px;">
<strong>Labels</strong> <strong>Labels</strong>
@if(hasWritePermission){ @if(hasWritePermission){
<div class="pull-right"> <div class="pull-right">
@helper.html.dropdown { @helper.html.dropdown() {
@labels.map { label => @labels.map { label =>
<li> <li>
<a href="#" class="toggle-label" data-label-id="@label.labelId"> <a href="#" class="toggle-label" data-label-id="@label.labelId">
@@ -134,7 +144,6 @@
</a> </a>
</li> </li>
} }
</ul>
} }
</div> </div>
} }

View File

@@ -1,7 +1,8 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(issues: List[(model.Issue, List[model.Label], Int)],
page: Int, page: Int,
labels: List[model.Label], collaborators: List[String],
milestones: List[model.Milestone], milestones: List[model.Milestone],
labels: List[model.Label],
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
allCount: Int, allCount: Int,
@@ -51,7 +52,7 @@
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title) <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){ @if(condition.milestoneId.isDefined){
<li> <li>
<a href="@condition.copy(milestoneId = None).toURL"> <a href="@condition.copy(milestoneId = None).toURL">
@@ -166,23 +167,6 @@
</ul> </ul>
</div> </div>
<table class="table table-bordered table-hover table-issues"> <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)&nbsp;
@if(commentCount > 0){
<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){ @if(issues.isEmpty){
<tr> <tr>
<td style="padding: 20px; background-color: #eee; text-align: center;"> <td style="padding: 20px; background-color: #eee; text-align: center;">
@@ -194,6 +178,68 @@
} }
</td> </td>
</tr> </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;">&nbsp;</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>
<li class="divider"></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>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId"><i class="icon-white"></i> @milestone.title</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"/>
}
<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.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)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@url(repository)/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
</label>
</td>
</tr>
} }
</table> </table>
<div class="pull-right"> <div class="pull-right">
@@ -201,6 +247,12 @@
</div> </div>
</div> </div>
</div> </div>
@if(hasWritePermission){
<form id="batcheditForm" method="POST">
<input type="hidden" name="value"/>
<input type="hidden" name="checked"/>
</form>
}
} }
@if(hasWritePermission){ @if(hasWritePermission){
<script> <script>
@@ -215,6 +267,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> </script>
} }

View File

@@ -81,7 +81,7 @@
</div> </div>
@if(milestone.description.isDefined){ @if(milestone.description.isDefined){
<div class="milestone-description"> <div class="milestone-description">
@markdown(milestone.description.get, repository, false, false, false) @markdown(milestone.description.get, repository, false, false)
</div> </div>
} }
</td> </td>

View File

@@ -19,15 +19,17 @@
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/> <link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/common/css/gitbucket.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/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/validation.js"></script>
<script src="@assets/common/js/gitbucket.js"></script> <script src="@assets/common/js/gitbucket.js"></script>
<script src="@assets/bootstrap/js/bootstrap.js"></script> <script src="@assets/bootstrap/js/bootstrap.js"></script>
<script src="@assets/datepicker/js/bootstrap-datepicker.js"></script> <script src="@assets/datepicker/js/bootstrap-datepicker.js"></script>
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script> <script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/google-code-prettify/prettify.js"></script> <script src="@assets/google-code-prettify/prettify.js"></script>
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
</head> </head>
<body> <body>
<div class="navbar navbar-inverse"> <div class="navbar">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container"> <div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
@@ -36,25 +38,27 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="brand" href="@path/">GitBucket</a> <a class="brand" href="@path/">GitBucket</a>
<div class="nav-collapse collapse"> <div class="nav-collapse collapse pull-right">
<ul class="nav pull-right"> @if(loginAccount.isDefined){
@if(loginAccount.isDefined){ <a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<li><a href="@path/new">New repo</a></li> <a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<li><a href="@url(loginAccount.get.userName)">Account</a></li> <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){ @if(loginAccount.get.isAdmin){
<li><a href="@path/admin/users">Administration</a></li> <a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
}
<li><a href="@path/signout">Sign out</a></li>
} else {
<li><a href="@path/signin?@currentUrl">Sign in</a></li>
} }
</ul> <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><!--/.nav-collapse -->
</div> </div>
</div> </div>
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
}
</div> </div>
<div class="container body"> <div class="container body">
@body @body
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,6 +1,7 @@
@()(implicit context: app.Context) @()(implicit context: app.Context)
@import context._ @import context._
@main("Create a New Repository"){ @main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true"> <form id="form" method="post" action="@path/new" validate="true">
<fieldset> <fieldset>
<label for="name"><strong>Repository name</strong></label> <label for="name"><strong>Repository name</strong></label>
@@ -9,10 +10,38 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="description"><strong>Description</strong> (optional)</label> <label for="description"><strong>Description</strong> (optional)</label>
<input type="text" name="description" id="description" style="width: 600px;"/> <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>
<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"/> <input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset> </fieldset>
</form> </form>
} </div>
}

View File

@@ -23,6 +23,7 @@
<tr> <tr>
<th style="font-weight: normal;"> <th style="font-weight: normal;">
<div class="pull-left"> <div class="pull-left">
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a> <a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
<span class="muted">@datetime(latestCommit.time)</span> <span class="muted">@datetime(latestCommit.time)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a> <a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>

View File

@@ -43,7 +43,9 @@
</tr> </tr>
<tr> <tr>
<td> <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 class="pull-right monospace small" style="text-align: right;">
<div> <div>
@if(commit.parents.size == 0){ @if(commit.parents.size == 0){
@@ -72,11 +74,14 @@
<div class="pull-right" style="margin-bottom: 10px;"> <div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/> <input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div> </div>
Showing @diffs.size changed @plural(diffs.size, "file")
@*
@if(diffs.size == 1){ @if(diffs.size == 1){
Showing 1 changed files Showing 1 changed file
} else { } else {
Showing @diffs.size changed files Showing @diffs.size changed files
} }
*@
</div> </div>
<ul id="commit-file-list" style="display: none;"> <ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>

View File

@@ -33,24 +33,27 @@
@day.map { commit => @day.map { commit =>
<tr> <tr>
<td> <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"> <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)/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> <a href="@url(repository)/tree/@commit.id" class="small">Browse code</a>
</div> </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> </td>
</tr> </tr>
} }

View File

@@ -10,6 +10,11 @@
@html.header("code", repository) @html.header("code", repository)
@tab(branch, repository, "files") @tab(branch, repository, "files")
<div class="head"> <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>&nbsp;
}
</div>
<a href="@url(repository)/tree/@branch">@repository.name</a> / <a href="@url(repository)/tree/@branch">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) => @pathList.zipWithIndex.map { case (section, i) =>
<a href="@url(repository)/tree/@branch/@pathList.take(i + 1).mkString("/")">@section</a> / <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> <a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
@if(latestCommit.description.isDefined){ @if(latestCommit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@latestCommit.id').toggle();" class="omit">...</a> <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> <pre id="description-@latestCommit.id" class="commit-description" style="display: none;">@link(latestCommit.description.get, repository)</pre>
} }
</th> </th>
@@ -31,6 +34,7 @@
<tr> <tr>
<td colspan="4" class="latest-commit"> <td colspan="4" class="latest-commit">
<div> <div>
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a> <a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
<span class="muted">@datetime(latestCommit.time)</span> <span class="muted">@datetime(latestCommit.time)</span>
<div class="pull-right align-right monospace"> <div class="pull-right align-right monospace">
@@ -73,7 +77,7 @@
@readme.map { content => @readme.map { content =>
<div class="box"> <div class="box">
<div class="box-header">README.md</div> <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> </div>
} }
} }

View File

@@ -0,0 +1,21 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@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>
}

View File

@@ -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 context._
@import view.helpers._ @import view.helpers._
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
@if(!hideBranchPulldown){
<li> <li>
<div class="btn-group" style="margin-right: 20px;"> <div class="btn-group" style="margin-right: 20px;">
<button class="btn dropdown-toggle" data-toggle="dropdown"> <button class="btn dropdown-toggle" data-toggle="dropdown">
@if(id.length == 40){ @if(id.length == 40){
tree: <strong>@id.substring(0, 10)</strong> tree: <strong>@id.substring(0, 10)</strong>
} else { }
@if(repository.branchList.contains(id)){
branch: <strong>@id</strong> branch: <strong>@id</strong>
} }
@if(repository.tags.exists(_.name == id)){
tag: <strong>@id</strong>
}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@@ -19,13 +25,14 @@
</ul> </ul>
</div> </div>
</li> </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=="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"> <li class="pull-right">
<div class="input-prepend"> <div class="input-append">
<span class="add-on">HTTP</span>
<input type="text" value="@repository.url" id="repository-url" readonly> <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> </div>
</li> </li>
</ul> </ul>

View File

@@ -3,7 +3,7 @@
@import view.helpers._ @import view.helpers._
@html.main(repository.owner + "/" + repository.name) { @html.main(repository.owner + "/" + repository.name) {
@html.header("code", 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> <h1>Tags</h1>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>

View File

@@ -1,9 +1,10 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Settings"){ @html.main("Settings"){
@html.header("settings", repository) @html.header("settings", repository)
@menu("options", repository){ @menu("options", repository){
@helper.html.information(info)
<form id="form" method="post" action="@url(repository)/settings/options" validate="true"> <form id="form" method="post" action="@url(repository)/settings/options" validate="true">
<div class="box"> <div class="box">
<div class="box-header">Settings</div> <div class="box-header">Settings</div>
@@ -22,17 +23,24 @@
</select> </select>
</fieldset> </fieldset>
<hr> <hr>
<fieldset> <fieldset class="margin">
<label><strong>Repository Type</strong></label> <label>
<label> <input type="radio" name="isPrivate" value="false" checked>
<input type="radio" name="isPrivate" value="false"@if(!repository.repository.isPrivate){ checked}> <strong>Public</strong><br>
<strong>Public</strong> - All users and guests can read this repository. <div>
</label> <span class="note">All users and guests can read this repository.</span>
<label> </div>
<input type="radio" name="isPrivate" value="true"@if(repository.repository.isPrivate){ checked}> </label>
<strong>Private</strong> - Only collaborators can read this repository. </fieldset>
</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>
</div> </div>
</div> </div>
@* @*

View File

@@ -2,22 +2,26 @@
@import context._ @import context._
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th class="metal">Sign in</th> <th class="metal">
@if(systemSettings.allowAccountRegistration){
<div class="pull-right">
<a href="@path/register" class="btn btn-mini">Create new account</a>
</div>
}
Sign in
</th>
</tr> </tr>
<tr> <tr>
<td> <td>
<form action="@path/signin" method="POST" validate="true"> <form action="@path/signin" method="POST" validate="true">
<label for="userName">Username</label> <label for="userName">Username</label>
<input type="text" name="userName" id="userName" style="width: 95%"/>
<span id="error-userName" class="error"></span> <span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" style="width: 95%"/>
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" name="password" id="password" style="width: 95%"/>
<span id="error-password" class="error"></span> <span id="error-password" class="error"></span>
<input type="password" name="password" id="password" style="width: 95%"/>
<div> <div>
<input type="submit" class="btn btn-success" value="Sign in"/> <input type="submit" class="btn btn-success" value="Sign in"/>
@if(systemSettings.allowAccountRegistration){
<a href="@path/register" class="btn">Create new account</a>
}
</div> </div>
</form> </form>
</td> </td>

View File

@@ -14,8 +14,8 @@
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div class="btn-group">
@if(pageName.isDefined){ @if(pageName.isDefined){
<a class="btn" href="@url(repository)/wiki/@pageName">View Page</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
<a class="btn" href="@url(repository)/wiki/@pageName/_history">Back to Page History</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Back to Page History</a>
} else { } else {
<a class="btn" href="@url(repository)/wiki/_history">Back to Wiki History</a> <a class="btn" href="@url(repository)/wiki/_history">Back to Wiki History</a>
} }

View File

@@ -13,9 +13,9 @@
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div class="btn-group">
@if(pageName != ""){ @if(pageName != ""){
<a class="btn" href="@url(repository)/wiki/@pageName">View Page</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
<a class="btn" href="@url(repository)/wiki/@pageName/_delete" id="delete">Delete Page</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
<a class="btn" href="@url(repository)/wiki/@pageName/_history">Page History</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
} }
</div> </div>
</li> </li>
@@ -23,7 +23,7 @@
<form action="@url(repository)/wiki/@if(pageName == ""){_new} else {_edit}" method="POST" validate="true"> <form action="@url(repository)/wiki/@if(pageName == ""){_new} else {_edit}" method="POST" validate="true">
<span id="error-pageName" class="error"></span> <span id="error-pageName" class="error"></span>
<input type="text" name="pageName" value="@pageName" style="width: 900px; font-weight: bold;" placeholder="Input a page name."/> <input type="text" name="pageName" value="@pageName" style="width: 900px; font-weight: bold;" placeholder="Input a page name."/>
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, "width: 900px; height: 400px;", "") @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "")
<input type="text" name="message" value="" style="width: 900px;" placeholder="Write a small message here explaining this change. (Optional)"/> <input type="text" name="message" value="" style="width: 900px;" placeholder="Write a small message here explaining this change. (Optional)"/>
<input type="hidden" name="currentPageName" value="@pageName"/> <input type="hidden" name="currentPageName" value="@pageName"/>
<input type="submit" value="Save" class="btn btn-success"> <input type="submit" value="Save" class="btn btn-success">

View File

@@ -23,9 +23,9 @@
<a class="btn" href="@url(repository)/wiki/_new">New Page</a> <a class="btn" href="@url(repository)/wiki/_new">New Page</a>
} }
} else { } else {
<a class="btn" href="@url(repository)/wiki/@pageName">View Page</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<a class="btn" href="@url(repository)/wiki/@pageName/_edit">Edit Page</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
} }
} }
</div> </div>
@@ -35,7 +35,7 @@
@commits.map { commit => @commits.map { commit =>
<tr> <tr>
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td> <td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
<td><a href="@url(commit.committer)">@commit.committer</a></td> <td>@avatar(commit.committer, 20)<a href="@url(commit.committer)">@commit.committer</a></td>
<td width="80%"> <td width="80%">
<span class="muted">@datetime(commit.time):</span> <span class="muted">@datetime(commit.time):</span>
@commit.shortMessage @commit.shortMessage
@@ -58,7 +58,7 @@
location.href = '@url(repository)/wiki/_compare/' + location.href = '@url(repository)/wiki/_compare/' +
$(e.get(1)).attr('value') + '...' + $(e.get(0)).attr('value'); $(e.get(1)).attr('value') + '...' + $(e.get(0)).attr('value');
} else { } else {
location.href = '@url(repository)/wiki/@pageName.get/_compare/' + location.href = '@url(repository)/wiki/@urlEncode(pageName.get)/_compare/' +
$(e.get(1)).attr('value') + '...' + $(e.get(0)).attr('value'); $(e.get(1)).attr('value') + '...' + $(e.get(0)).attr('value');
} }
} }

View File

@@ -15,19 +15,16 @@
<div class="btn-group"> <div class="btn-group">
@if(hasWritePermission){ @if(hasWritePermission){
<a class="btn" href="@url(repository)/wiki/_new">New Page</a> <a class="btn" href="@url(repository)/wiki/_new">New Page</a>
<a class="btn" href="@url(repository)/wiki/@pageName/_edit">Edit Page</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
} }
<a class="btn" href="@url(repository)/wiki/@pageName/_history">Page History</a> <a class="btn" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
</div> </div>
</li> </li>
</ul> </ul>
<div class="markdown-body"> <div class="markdown-body">
@markdown(page.content, repository, true, false, false) @markdown(page.content, repository, true, false)
</div> </div>
<div class="small"> <div class="small">
<span class="muted">Last edited by @page.committer at @datetime(page.time)</span> <span class="muted">Last edited by @page.committer at @datetime(page.time)</span>
</div> </div>
} }
<script>
$(function(){ prettyPrint(); });
</script>

View File

@@ -18,7 +18,7 @@
</ul> </ul>
<ul> <ul>
@pages.map { page => @pages.map { page =>
<li><a href="@url(repository)/wiki/@page">@page</a></li> <li><a href="@url(repository)/wiki/@urlEncode(page)">@page</a></li>
} }
</ul> </ul>

View File

@@ -6,9 +6,9 @@
<li@if(active == "pages"){ class="active"}><a href="@url(repository)/wiki/_pages">Pages</a></li> <li@if(active == "pages"){ class="active"}><a href="@url(repository)/wiki/_pages">Pages</a></li>
<li@if(active == "history"){ class="active"}><a href="@url(repository)/wiki/_history">Wiki History</a></li> <li@if(active == "history"){ class="active"}><a href="@url(repository)/wiki/_history">Wiki History</a></li>
<li class="pull-right"> <li class="pull-right">
<div class="input-prepend"> <div class="input-append">
<span class="add-on">HTTP</span>
<input type="text" value="@repository.url.replaceFirst("\\.git$", ".wiki.git")" readonly id="repository-url"> <input type="text" value="@repository.url.replaceFirst("\\.git$", ".wiki.git")" readonly id="repository-url">
<span id="repository-url-copy" class="add-on btn" data-clipboard-text="@repository.url.replaceFirst("\\.git$", ".wiki.git")" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
</div> </div>
</li> </li>
</ul> </ul>

View File

@@ -4,13 +4,20 @@
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"> version="3.0">
<!-- ===================================================================== -->
<!-- Session configuration -->
<!-- ===================================================================== -->
<listener>
<listener-class>servlet.SessionCleanupListener</listener-class>
</listener>
<!-- ===================================================================== --> <!-- ===================================================================== -->
<!-- Scalatra configuration --> <!-- Scalatra configuration -->
<!-- ===================================================================== --> <!-- ===================================================================== -->
<listener> <listener>
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class> <listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener> </listener>
<servlet> <servlet>
<servlet-name>GitRepositoryServlet</servlet-name> <servlet-name>GitRepositoryServlet</servlet-name>
<servlet-class>servlet.GitRepositoryServlet</servlet-class> <servlet-class>servlet.GitRepositoryServlet</servlet-class>

View File

@@ -5,6 +5,38 @@ body {
color: #333; color: #333;
} }
div.navbar-inner {
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid #d4d4d4;
padding-right: 0px;
}
div.nav-collapse a.menu {
margin-right: 12px;
line-height: 40px;
}
div.nav-collapse a.btn-last {
margin-right: 30px;
}
div.nav-collapse a.menu-last {
margin-right: 30px;
line-height: 40px;
}
div.gitbucket-version {
font-size: small;
color: silver;
float: right;
margin-right: 10px;
}
table.global-nav { table.global-nav {
width: 920px; width: 920px;
margin-bottom: 10px; margin-bottom: 10px;
@@ -97,7 +129,7 @@ div.block-header {
div.block { div.block {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid silver; border-bottom: 1px solid #ddd;
} }
h3 { h3 {
@@ -123,7 +155,7 @@ div.box-header {
border-top-right-radius: 1px; border-top-right-radius: 1px;
border: 1px solid #d8d8d8; border: 1px solid #d8d8d8;
border-bottom: none; border-bottom: none;
padding: 10px 10px 11px 10px; padding: 8px 10px 8px 10px;
text-shadow: 0 1px 0 #fff text-shadow: 0 1px 0 #fff
} }
@@ -186,6 +218,34 @@ hr {
margin-bottom: 4px; margin-bottom: 4px;
} }
span.note {
margin-left: 20px;
}
img.avatar {
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
}
div.activity-message {
margin-left: 20px;
}
fieldset.margin {
border-top: 1px solid #eee;
margin-top: 10px;
padding-top: 10px;
}
div.account-image {
text-align: center;
margin-bottom: 8px;
}
/****************************************************************************/
/* Sign-in form */
/****************************************************************************/
div.signin-form { div.signin-form {
width: 350px; width: 350px;
margin: 30px auto; margin: 30px auto;
@@ -270,6 +330,15 @@ th, td, .table th, .table td {
padding-bottom: 4px; padding-bottom: 4px;
} }
div.commit-avatar-image {
float: left;
margin-right: 4px;
}
div.commit-message-box {
margin-left: 42px;
}
pre.commit-description { pre.commit-description {
font-weight: normal; font-weight: normal;
border: none; border: none;
@@ -437,9 +506,18 @@ h4#issueTitle {
padding: 0px; padding: 0px;
} }
div.issue-avatar-image {
float: left;
}
div.issue-box {
margin-bottom: 15px;
margin-left: 50px;
}
div.issue-comment-box { div.issue-comment-box {
margin-bottom: 15px; margin-bottom: 15px;
margin-top: 25px; margin-left: 50px;
} }
div.issue-comment-action { div.issue-comment-action {
@@ -478,6 +556,7 @@ ul.collaborator li {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 3px; border-radius: 3px;
padding: 6px; padding: 6px;
margin-bottom: 2px;
} }
ul.collaborator li:hover { ul.collaborator li:hover {

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,44 @@
$(function(){ $(function(){
$.ajaxSetup({ // disable Ajax cache
cache: false $.ajaxSetup({ cache: false });
});
$('#repository-url').click(function(){ $('#repository-url').click(function(){
this.select(0, this.value.length); this.select(0, this.value.length);
}); });
$('img[data-toggle=tooltip]').tooltip();
$('a[data-toggle=tooltip]').tooltip();
// copy to clipboard
(function() {
// Find ZeroClipboard.swf file URI from ZeroClipboard JavaScript file path.
// NOTE(tanacasino) I think this way is wrong... but i don't know correct way.
var moviePath = (function() {
var zclipjs = "ZeroClipboard.min.js";
var scripts = document.getElementsByTagName("script");
var i = scripts.length;
while(i--) {
var match = scripts[i].src.match(zclipjs + "$");
if(match) {
return match.input.substr(0, match.input.length - 6) + 'swf';
}
}
})();
var clip = new ZeroClipboard($("#repository-url-copy"), {
moviePath: moviePath
});
var title = $('#repository-url-copy').attr('title');
$('#repository-url-copy').removeAttr('title')
clip.on('complete', function(client, args) {
$(clip.htmlBridge).attr('title', 'copied!').tooltip('fixTitle').tooltip('show');
$(clip.htmlBridge).attr('title', title).tooltip('fixTitle');
});
$(clip.htmlBridge).tooltip({
title: title,
placement: $('#repository-url-copy').attr('data-placement')
});
})();
// syntax highlighting by google-code-prettify
prettyPrint(); prettyPrint();
}); });

View File

@@ -2,6 +2,12 @@ $(function(){
$.each($('form[validate=true]'), function(i, form){ $.each($('form[validate=true]'), function(i, form){
$(form).submit(validate); $(form).submit(validate);
}); });
$.each($('input[formaction]'), function(i, input){
$(input).click(function(){
var form = $(input).parents('form')
$(form).attr('action', $(input).attr('formaction'))
});
});
}); });
function validate(e){ function validate(e){
@@ -19,6 +25,7 @@ function validate(e){
form.data('validated', true); form.data('validated', true);
form.submit(); form.submit();
} else { } else {
form.data('validated', false);
displayErrors(data); displayErrors(data);
} }
}, 'json'); }, 'json');

File diff suppressed because one or more lines are too long

Binary file not shown.