Compare commits

...

204 Commits
1.1 ... 1.4

Author SHA1 Message Date
takezoe
fd84b3f1c4 Fix link path in dashborad. 2013-07-31 12:04:09 +09:00
Naoki Takezoe
9d4a052ecc Update for 1.4 release. 2013-07-31 00:49:27 +09:00
takezoe
f93c8965be Bug fix. 2013-07-30 22:03:48 +09:00
takezoe
beef86ce8c Extend session timeout to 24 hours. 2013-07-30 21:41:47 +09:00
shimamoto
03b75d5379 (refs #26) Fix splitWith condition. 2013-07-30 21:08:38 +09:00
shimamoto
66855e65bb (refs #26) Implements repository filter. 2013-07-30 19:36:20 +09:00
takezoe
b8da93912f Fix query in RepositoryService#getVisibleRepositories fluently :-) 2013-07-30 12:42:42 +09:00
takezoe
d675115615 Remove unnecessary <hr>. 2013-07-30 11:59:47 +09:00
shimamoto
0296a0bde6 Merge branch 'master' of https://github.com/takezoe/gitbucket.git
Conflicts:
	src/main/scala/app/DashboardController.scala
2013-07-30 10:51:44 +09:00
shimamoto
8409384232 (refs #26) Implements the dashboard issue display. 2013-07-30 10:47:46 +09:00
Naoki Takezoe
cfaee56a08 Merge pull request #55 from tanacasino/fix/improve-repository-viewer
Make more fast and github-like repository viewer
2013-07-29 15:20:00 -07:00
takezoe
7d65717784 The method of RepositoryService was cleaned up. 2013-07-30 03:41:47 +09:00
Tomofumi Tanaka
7079d50fdf Make more fast and github-like repository viewer 2013-07-30 03:26:36 +09:00
takezoe
41a613e151 Move private method. 2013-07-30 02:45:14 +09:00
takezoe
1f2b6a0acc Adjust whitespaces. 2013-07-29 22:58:30 +09:00
takezoe
25d402c9d1 (refs #53)Fix path extraction for branch which contains '/'. 2013-07-29 22:57:57 +09:00
takezoe
045b7cf019 Fix avatar problem. 2013-07-29 17:10:45 +09:00
takezoe
57109dd72e Add init-param 'webAllowOthers' to web.xml. 2013-07-29 13:24:01 +09:00
takezoe
1b878b59b8 Use ISSUE_OUTLINE_VIEW to retrieve comment count. 2013-07-26 02:59:51 +09:00
Naoki Takezoe
80452ab4cd Merge pull request #52 from tanacasino/fix/set-initial-commiter
Initial commiter should be repository creator
2013-07-25 10:36:50 -07:00
Tomofumi Tanaka
4d9c8e8d3e Initial commiter should be repository creator
like wiki repository.
Now it seems that set $HOME/.gitconfig user.name,user.email or
machine username and username@hostname.
2013-07-26 00:29:00 +09:00
shimamoto
a5f12a50e6 Add view ISSUE_OUTLINE_VIEW. 2013-07-25 20:28:19 +09:00
takezoe
07ef06ad95 Improve authentication for H2 console. 2013-07-25 03:16:34 +09:00
takezoe
34e2663492 Use JGitUtil.isEmpty() to check whether repository is empty. 2013-07-24 23:10:55 +09:00
Naoki Takezoe
8b90f87589 Merge pull request #51 from tanacasino/fix/search-in-empty-repository
(refs #50)Fix search logic in empty repository
2013-07-24 06:14:42 -07:00
takezoe
8c1e45da6c Set initial value of 'owner' parameter in the repository creation page. 2013-07-24 22:09:10 +09:00
Tomofumi Tanaka
62a6d74393 (refs #50)Fix search logic in empty repository 2013-07-24 16:46:46 +09:00
shimamoto
cb94447290 (refs #26) Add Dashboard controller. Uses a common design at issue. 2013-07-24 14:10:17 +09:00
shimamoto
e4cf509d0f Add tab at dashboard. 2013-07-24 14:01:10 +09:00
takezoe
3a7391fbb3 (refs #8)Some fix for group management. 2013-07-24 03:36:42 +09:00
takezoe
2155734e23 (refs #8)Add Members tab to account information page for group account. 2013-07-24 02:12:35 +09:00
takezoe
6806e66d64 (refs #8)Change create/edit user template name and path. 2013-07-24 02:04:08 +09:00
takezoe
db8305b5e9 (refs #8)Change create/edit user template name and path. 2013-07-24 02:03:42 +09:00
takezoe
e8330eedc3 (refs #8)Group repository creation is completed. 2013-07-24 02:00:52 +09:00
takezoe
c01c4a860c (refs #8)Set initial value for editing group. 2013-07-24 01:54:33 +09:00
takezoe
6e778f209d (refs #8)Fix error message position. 2013-07-24 01:42:40 +09:00
takezoe
b760361184 (refs #8)Implementing repository creation for group. 2013-07-23 22:05:30 +09:00
takezoe
7150befa54 (refs #8)Group register/edit form is completed. 2013-07-23 18:52:36 +09:00
takezoe
5bf0b275cb (refs #8)Remove unused code. 2013-07-23 15:39:47 +09:00
takezoe
c86bf1d68b (refs #8)Merge user name proposal API to IndexController. 2013-07-23 15:37:59 +09:00
takezoe
e61bde1415 (refs #8)Implementing group register/edit form. 2013-07-23 13:02:30 +09:00
takezoe
e4b3f0ddef (refs #8)Implementing group register/edit form. 2013-07-23 11:59:49 +09:00
takezoe
ec73294900 (refs #8)Add model for GROUP_MEMBER. 2013-07-23 11:08:36 +09:00
takezoe
30eb949ce1 (refs #8)Start to implement group management. 2013-07-22 22:22:49 +09:00
takezoe
f5d69a3df6 Merge branch 'master' into group-management 2013-07-22 21:22:36 +09:00
takezoe
3cc39489bd (refs #40)Enable H2 Console. 2013-07-22 21:12:22 +09:00
takezoe
ace5d7de9e (refs #3)Separate search actions to SearchController. 2013-07-22 17:28:13 +09:00
takezoe
1682eb3915 (refs #8)Add DDL to add new table and columns for group management. 2013-07-22 17:15:12 +09:00
takezoe
6fd1a990ae (refs #44)Add milestone progress bar to the issue detail page. 2013-07-22 17:01:00 +09:00
Naoki Takezoe
cfa36a21b5 Merge pull request #47 from tomykaira/set_icon_on_assignee_change
Set icon when assignee is changed in page (via JS)
2013-07-21 20:29:09 -07:00
takezoe
95163d4864 Add link to the account info page for the assigned user icon at the issue list. 2013-07-22 12:27:18 +09:00
takezoe
5a9645829d (refs #33)Small fix for pull request #45. 2013-07-22 12:25:28 +09:00
takezoe
be78d93c1f Fix avatar tooltip. 2013-07-22 12:22:18 +09:00
Naoki Takezoe
ac63558645 Merge pull request #45 from tomykaira/feature/participants
Add participants to issue detail
2013-07-21 20:10:57 -07:00
tomykaira
88fb2e49dc Set icon when assignee is changed in page
The javascript code did not set icon, whereas the view script does.
2013-07-21 20:06:24 +09:00
tomykaira
6e96ad0f17 (refs #37)Add participants to issue detail
The design is inherited from Github.
2013-07-21 18:25:02 +09:00
takezoe
e54754d04f (refs #25)Display due date in milestone dropdown chooser. 2013-07-21 01:39:38 +09:00
takezoe
e4b2ebe2a4 (refs #25)Alert if due date passed. 2013-07-20 19:34:58 +09:00
takezoe
0028431dde Exclude some actions from comment count at the repository search result. 2013-07-20 03:06:33 +09:00
takezoe
91d94de1d2 Merge branch '#3_repository-search'
Conflicts:
	src/main/scala/app/UserManagementController.scala
	src/main/scala/service/IssuesService.scala
	src/main/twirl/issues/issue.scala.html
2013-07-20 03:00:16 +09:00
takezoe
0c131ec990 Move FileUploadUtil to FileUploadControllerBase. 2013-07-19 20:33:40 +09:00
takezoe
54280d5572 Add paginator and separate search code in controller to service. 2013-07-19 20:24:31 +09:00
Naoki Takezoe
6d3640a8b0 Merge pull request #43 from rabitarochan/fix/wiki-resourceleak
Fix resource leak.
2013-07-19 04:23:57 -07:00
rabitarochan
8226073506 Fix resource leak. 2013-07-19 18:18:44 +09:00
takezoe
f4a5e18c69 Merge branch 'repository-search-cache' into #3_repository-search 2013-07-19 18:04:28 +09:00
takezoe
133af93548 Don't use cache library immediately. 2013-07-19 18:03:48 +09:00
takezoe
3546a5d392 Ignore error in activity timeline caused by invalid data. 2013-07-19 14:11:13 +09:00
takezoe
fb921e951e Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-07-19 14:08:05 +09:00
shimamoto
22685d8e3b Add reference link to wiki. 2013-07-19 13:25:23 +09:00
takezoe
b2d050d136 Remove unused import statement. 2013-07-19 12:17:40 +09:00
takezoe
e3ff1dcd96 Check state of repository type radio button had not been applied. 2013-07-19 03:48:58 +09:00
takezoe
897890e1b4 (refs #42)Requires BASIC authentication for /info/refs?service=git-receive-pack. 2013-07-19 03:23:19 +09:00
Naoki Takezoe
2c95ea00e8 GitBucket 1.3 released. 2013-07-18 20:59:59 +09:00
takezoe
00d6ed7dbb Cleanup search field and global header styles. 2013-07-18 20:50:42 +09:00
shimamoto
b23133c79c Move Implicit && to model package object. 2013-07-18 20:21:24 +09:00
takezoe
eef2f26707 Change highlight color. 2013-07-18 20:12:08 +09:00
takezoe
7483ad1732 Pagination for repository search results. 2013-07-18 20:02:12 +09:00
shimamoto
188237db24 Replace String#format() with string interpolation. 2013-07-18 19:54:07 +09:00
takezoe
134624967b Store search results into singleton cache. 2013-07-18 19:09:21 +09:00
takezoe
a1b8d1cd84 Add Guava to use CacheBuilder. 2013-07-18 19:08:03 +09:00
takezoe
e7b9293f3b Merge branch 'master' into #3_repository-search 2013-07-18 17:07:22 +09:00
takezoe
93e4a8931d (refs #3)Add search form to all repository related pages. 2013-07-18 17:06:28 +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
dedf5094c1 Small fix and add TODO. 2013-07-18 03:58:39 +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
takezoe
e4d97e4059 (refs #3)Hide result count if count is zero. 2013-07-18 01:34:06 +09:00
takezoe
ba567d81cb (refs #3)Add result count to the menu. 2013-07-18 01:12:12 +09:00
takezoe
4fb6005f44 (refs #3)Apply likeEncode to search keyword. 2013-07-18 00:47:55 +09:00
takezoe
69ec4175eb (refs #3)Fix issue search condition. 2013-07-17 21:18:09 +09:00
takezoe
d46e90dcdb (refs #3)Improve presentation for code search results. 2013-07-17 21:13:53 +09:00
takezoe
900e91e101 Bugfix 2013-07-17 19:00:35 +09:00
takezoe
05d7e33d86 (refs #3)Add search form at the top of search result. 2013-07-17 18:32:59 +09:00
takezoe
7f0aff8c03 (refs #3)Cleanup 2013-07-17 16:59:38 +09:00
takezoe
512e59193d (refs #3)Issue search is temporary available. 2013-07-17 16:47:43 +09:00
shimamoto
8f056e4a82 Finished the issues controller refactoring. 2013-07-17 15:36:05 +09:00
takezoe
d06a986293 Remove unused import statement. 2013-07-17 13:57:45 +09:00
shimamoto
d866847c0d (refs #11) Fix comments to display. 2013-07-17 13:56:54 +09:00
takezoe
83472bc354 Remove unused import statement. 2013-07-17 13:55:26 +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
ce8168d97a Fix typo. 2013-07-17 11:54:10 +09:00
takezoe
27670525a3 (refs #3)Search by AND if query words are separated by whitespace. 2013-07-17 11:52:28 +09:00
takezoe
4796d7f450 (refs #3)Search git repository without cloning to the file system. 2013-07-17 06:36:21 +09:00
takezoe
79ec96343f (refs #3)Start work for repository search. 2013-07-17 03:24:47 +09:00
takezoe
4572a455c8 Change ISSUE_COMMENT.ACTION to NOT NULL. 2013-07-16 23:05:45 +09:00
takezoe
cb591925ea (refs #3)Add search field to header area. 2013-07-16 21:58:09 +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
118 changed files with 5325 additions and 1170 deletions

4
.gitignore vendored
View File

@@ -14,3 +14,7 @@ project/plugins/project/
.classpath
.project
.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 is a Github clone by Scala, Easy to setup.
GitBucket is the easily installable Github clone written with Scala.
The current version of GitBucket provides a basic features below:
@@ -9,12 +9,12 @@ The current version of GitBucket provides a basic features below:
- Repository viewer (some advanced features are not implemented)
- Wiki
- Issues
- Activity timeline
- User management (for Administrators)
Following features are not implemented, but we will make them in the future release!
- Fork and pull request
- Timeline
- Search
- Network graph
- Statics
@@ -32,9 +32,39 @@ Installation
The default administrator account is **root** and password is **root**.
To upgrade GitBucket, only replace gitbucket.war.
Release Notes
--------
### 1.4 - 31 Jul 2013
- Group management.
- Repository search for code and issues.
- Display user related issues on the dashboard.
- Display participants avatar of issues on the issue page.
- Performance improvement for repository viewer.
- Alert by milestone due date.
- H2 database administration console.
- Fixed some bugs.
### 1.3 - 18 Jul 2013
- Batch updating for issues.
- Display assigned user on issue list.
- User icon and Gravatar support.
- Convert @xxxx to link to the account page.
- Add copy to clipboard button for git clone URL.
- Allows multi-byte characters as wiki page name.
- Allows to create the empty repository.
- Fixed some bugs.
### 1.2 - 09 Jul 2013
- Added activity timeline.
- Bugfix for Git 1.8.1.5 or later.
- Allows multi-byte characters as label.
- Fixed some bugs.
### 1.1 - 05 Jul 2013
- Fixed some bugs.
- Upgrade to JGit 3.0.
### 1.0 - 04 Jul 2013
- This is a first public release.

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
java -Dhttp.proxyHost=proxy.intellilink.co.jp -Dhttp.proxyPort=8080 -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*

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

@@ -0,0 +1,24 @@
CREATE TABLE GROUP_MEMBER(
GROUP_NAME VARCHAR(100) NOT NULL,
USER_NAME VARCHAR(100) NOT NULL
);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);

View File

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

View File

@@ -1,31 +1,40 @@
package app
import service._
import util.OneselfAuthenticator
import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._
import util.Directory._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
class AccountController extends AccountControllerBase
with SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator
with SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator
trait AccountControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with RepositoryService with OneselfAuthenticator =>
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String,mailAddress: String, url: Option[String])
case class AccountNewForm(userName: String, password: String,mailAddress: String,
url: Option[String], fileId: Option[String])
case class AccountEditForm(password: Option[String], mailAddress: String, url: Option[String])
case class AccountEditForm(password: Option[String], mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
)(AccountNewForm.apply)
val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply)
/**
@@ -33,52 +42,74 @@ trait AccountControllerBase extends ControllerBase {
*/
get("/:userName") {
val userName = params("userName")
getAccountByUserName(userName).map {
account.html.info(_, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
getAccountByUserName(userName).map { account =>
params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" =>
_root_.account.html.activity(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) =>
_root_.account.html.members(account, getGroupMembers(account.userName))
// Repositories
case _ =>
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
}
} getOrElse NotFound
}
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image =>
contentType = FileUtil.getMimeType(image)
new java.io.File(getUserUploadDir(userName), image)
} getOrElse {
contentType = "image/png"
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
}
}
get("/:userName/_edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map(x => account.html.edit(Some(x))) getOrElse NotFound
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
})
post("/:userName/_edit", editForm)(oneselfOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { account =>
updateAccount(account.copy(
password = form.password.map(encrypt).getOrElse(account.password),
password = form.password.map(sha1).getOrElse(account.password),
mailAddress = form.mailAddress,
url = form.url))
redirect("/%s".format(userName))
updateImage(userName, form.fileId, form.clearImage)
flash += "info" -> "Account information has been updated."
redirect(s"/${userName}/_edit")
} getOrElse NotFound
})
get("/register"){
if(loadSystemSettings().allowAccountRegistration){
account.html.edit(None)
if(context.loginAccount.isDefined){
redirect("/")
} else {
account.html.edit(None, None)
}
} else NotFound
}
post("/register", newForm){ newForm =>
post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){
createAccount(newForm.userName, encrypt(newForm.password), newForm.mailAddress, false, newForm.url)
createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/signin")
} else NotFound
}
// TODO Merge with UserManagementController
private def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
// TODO Merge with UserManagementController
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
}

View File

@@ -1,24 +1,58 @@
package app
import model.Account
import util.Validations
import _root_.util.Directory._
import _root_.util.{StringUtil, FileUtil, Validations}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model.Account
import scala.Some
import service.AccountService
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
/**
* Provides generic features for ScalatraServlet implementations.
* Provides generic features for controller implementations.
*/
abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
implicit val jsonFormats = DefaultFormats
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse]
val context = request.getServletContext.getContextPath
val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]
if(account == null){
// Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + path)
} else if(account.isAdmin){
// H2 Console (administrators only)
chain.doFilter(request, response)
} else {
// Redirect to dashboard
httpResponse.sendRedirect(context + "/")
}
} else if(path.startsWith("/git/")){
// Git repository
chain.doFilter(request, response)
} else {
// Scalatra actions
super.doFilter(request, response, chain)
}
}
/**
* Returns the context object for the request.
*/
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL)
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
private def currentURL: String = {
val queryString = request.getQueryString
@@ -87,4 +121,91 @@ abstract class ControllerBase extends ScalatraFilter
}
case class Context(path: String, loginAccount: Option[Account], currentUrl: String)
/**
* Context object for the current request.
*/
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
/**
* Get object from cache.
*
* If object has not been cached with the specified key then retrieves by given action.
* Cached object are available during a request.
*/
def cache[A](key: String)(action: => A): A = {
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse {
val newObject = action
request.setAttribute("cache." + key, newObject)
newObject
}
}
}
/**
* Base trait for controllers which manages account information.
*/
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
if(clearImage){
getAccountByUserName(userName).flatMap(_.image).map { image =>
new java.io.File(getUserUploadDir(userName), image).delete()
updateAvatarImage(userName, None)
}
} else {
fileId.map { fileId =>
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile(
getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename)
)
updateAvatarImage(userName, Some(filename))
}
}
}
protected def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
}
/**
* Base trait for controllers which needs file uploading feature.
*/
trait FileUploadControllerBase {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
if(filename.isDefined){
session.removeAttribute("upload_" + fileId)
}
filename
}
}

View File

@@ -1,99 +1,123 @@
package app
import util.Directory._
import util.UsersAuthenticator
import util.{JGitUtil, UsersAuthenticator}
import service._
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with UsersAuthenticator
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with WikiService with LabelsService with UsersAuthenticator =>
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator =>
case class RepositoryCreationForm(name: String, description: Option[String])
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
val form = mapping(
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text())))
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
html.newrepo()
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", form)(usersOnly { form =>
val loginUserName = context.loginAccount.get.userName
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, loginUserName, form.description)
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Insert default labels
createLabel(loginUserName, form.name, "bug", "fc2929")
createLabel(loginUserName, form.name, "duplicate", "cccccc")
createLabel(loginUserName, form.name, "enhancement", "84b6eb")
createLabel(loginUserName, form.name, "invalid", "e6e6e6")
createLabel(loginUserName, form.name, "question", "cc317c")
createLabel(loginUserName, form.name, "wontfix", "ffffff")
createLabel(form.owner, form.name, "bug", "fc2929")
createLabel(form.owner, form.name, "duplicate", "cccccc")
createLabel(form.owner, form.name, "enhancement", "84b6eb")
createLabel(form.owner, form.name, "invalid", "e6e6e6")
createLabel(form.owner, form.name, "question", "cc317c")
createLabel(form.owner, form.name, "wontfix", "ffffff")
// Create the actual repository
val gitdir = getRepositoryDir(loginUserName, form.name)
val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
repository.create
if(form.createReadme){
val tmpdir = getInitRepositoryDir(form.owner, form.name)
try {
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
// Create README.md
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}, "UTF-8")
val tmpdir = getInitRepositoryDir(loginUserName, form.name)
try {
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
// Create README.md
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
if(form.description.nonEmpty){
form.name + "\n===============\n\n" + form.description.get
} else {
form.name + "\n===============\n"
}, "UTF-8")
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit.setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit.setCommitter(loginAccount.userName, loginAccount.mailAddress).setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
}
}
// Create Wiki repository
createWikiRepository(context.loginAccount.get, form.name)
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
// redirect to the repository
redirect("/%s/%s".format(loginUserName, form.name))
redirect(s"/${form.owner}/${form.name}")
})
private def existsAccount: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
/**
* Duplicate check for the repository name.
*/
private def unique: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}
}

View File

@@ -0,0 +1,57 @@
package app
import service._
import util.UsersAuthenticator
class DashboardController extends DashboardControllerBase
with IssuesService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly {
searchIssues("all")
})
get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned")
})
get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by")
})
private def searchIssues(filter: String) = {
import IssuesService._
// condition
val sessionKey = "dashboard/issues"
val condition = if(request.getQueryString == null)
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
else IssueSearchCondition(request)
session.put(sessionKey, condition)
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
//
dashboard.html.issues(
issues.html.listparts(
searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page,
countIssue(condition.copy(state = "open"), filterUser, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, repositories: _*),
condition),
countIssue(condition, Map.empty, repositories: _*),
countIssue(condition, Map("assigned" -> userName), repositories: _*),
countIssue(condition, Map("created_by" -> userName), repositories: _*),
countIssueGroupByRepository(condition, filterUser, repositories: _*),
condition,
filter)
}
}

View File

@@ -0,0 +1,32 @@
package app
import util.{FileUtil}
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils
/**
* Provides Ajax based file upload functionality.
*
* This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
class FileUploadController extends ScalatraServlet
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => {
val fileId = generateFileId
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
session += "upload_" + fileId -> file.name
Ok(fileId)
}
case None => BadRequest
}
}
}

View File

@@ -1,14 +1,38 @@
package app
import util._
import service._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase with RepositoryService with AccountService with SystemSettingsService
class IndexController extends IndexControllerBase
with RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator =>
trait IndexControllerBase extends ControllerBase { self: RepositoryService with SystemSettingsService =>
get("/"){
html.index(getAccessibleRepositories(context.loginAccount, baseUrl), loadSystemSettings(),
context.loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil))
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
)
}
}
/**
* JSON API for collaborator completion.
*
* TODO Move to other controller?
*/
get("/_user/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
)
})
}

View File

@@ -8,19 +8,18 @@ import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAut
import org.scalatra.Ok
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with LabelsService with MilestonesService
self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))),
@@ -40,6 +39,11 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(label("Comment", text(required)))
)(CommentForm.apply)
val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _)
})
@@ -63,7 +67,7 @@ trait IssuesControllerBase extends ControllerBase {
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
getMilestones(owner, name),
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
@@ -86,11 +90,14 @@ trait IssuesControllerBase extends ControllerBase {
val owner = repository.owner
val name = repository.name
val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName
val issueId = createIssue(owner, name, context.loginAccount.get.userName, form.title, form.content,
// insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None)
// insert labels
if(writable){
form.labelNames.map { value =>
val labels = getLabels(owner, name)
@@ -102,7 +109,10 @@ trait IssuesControllerBase extends ControllerBase {
}
}
redirect("/%s/%s/issues/%d".format(owner, name, issueId))
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
redirect(s"/${owner}/${name}/issues/${issueId}")
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
@@ -112,28 +122,21 @@ trait IssuesControllerBase extends ControllerBase {
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
updateIssue(owner, name, issue.issueId, form.title, form.content)
redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId))
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner
val name = repository.name
handleComment(form.issueId, Some(form.content), repository)() map { id =>
redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
getIssue(owner, name, form.issueId.toString).map { issue =>
redirect("/%s/%s/issues/%d#comment-%d".format(
owner, name, form.issueId,
createComment(owner, name, context.loginAccount.get.userName,
form.issueId,
form.content,
if(isEditable(owner, name, issue.openedUserName)){
params.get("action") filter { action =>
updateClosed(owner, name, form.issueId, if(action == "close") true else false) > 0
}
} else None)
))
}
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { id =>
redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
@@ -143,7 +146,7 @@ trait IssuesControllerBase extends ControllerBase {
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId))
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
})
@@ -159,7 +162,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true)
repository, false, true)
))
}
} else Unauthorized
@@ -176,7 +179,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true)
repository, false, true)
))
}
} else Unauthorized
@@ -198,32 +201,109 @@ trait IssuesControllerBase extends ControllerBase {
})
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt,
params.get("assignedUserName") filter (_.trim != ""))
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
Ok("updated")
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt,
params.get("milestoneId") collect { case x if x.trim != "" => x.toInt })
Ok("updated")
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
} getOrElse NotFound
} getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
val action = params.get("value")
executeBatch(repository) {
handleComment(_, None, repository)( _ => action)
}
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
val labelId = params("value").toInt
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
}
}
})
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
val value = assignedUserName("value")
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value)
}
})
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
val value = milestoneId("value")
executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value)
}
})
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues")
}
/**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
val owner = repository.owner
val name = repository.name
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") -> Some(recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
t
}
.getOrElse(None -> None)
val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get )
match {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
}
// record activity
content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
commentId
}
}
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner
val repoName = repository.name
val userName = if(filter != "all") Some(params("userName")) else None
val sessionKey = "%s/%s/issues".format(owner, repoName)
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = s"${owner}/${repoName}/issues"
// retrieve search condition
val condition = if(request.getQueryString == null){
@@ -233,16 +313,17 @@ trait IssuesControllerBase extends ControllerBase {
session.put(sessionKey, condition)
issues.html.list(
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
countIssue(owner, repoName, condition, "all", None),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))),
countIssueGroupByLabels(owner, repoName, condition, filter, userName),
countIssue(condition.copy(state = "open"), filterUser, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, owner -> repoName),
countIssue(condition, Map.empty, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition,
filter,
repository,

View File

@@ -13,18 +13,18 @@ trait LabelsControllerBase extends ControllerBase {
case class LabelForm(labelName: String, color: String)
val newForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, identifier, maxlength(100)))),
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
val editForm = mapping(
"editLabelName" -> trim(label("Label name", text(required, identifier, maxlength(100)))),
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"editColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect("/%s/%s/issues".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues")
})
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
@@ -47,4 +47,18 @@ trait LabelsControllerBase extends ControllerBase {
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
})
/**
* Constraint for the identifier such as user name, repository name or page name.
*/
private def labelName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
if(!value.matches("^[^,]+$")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
}

View File

@@ -3,7 +3,7 @@ package app
import jp.sf.amateras.scalatra.forms._
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
@@ -35,7 +35,7 @@ trait MilestonesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
})
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
@@ -45,28 +45,28 @@ trait MilestonesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
closeMilestone(milestone)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
openMilestone(milestone)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
} getOrElse NotFound
})

View File

@@ -5,11 +5,12 @@ import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
class SettingsController extends SettingsControllerBase
class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
trait SettingsControllerBase extends ControllerBase {
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator =>
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
@@ -30,14 +31,14 @@ trait SettingsControllerBase extends ControllerBase {
* Redirect to the Options page.
*/
get("/:owner/:repository/settings")(ownerOnly { repository =>
redirect("/%s/%s/settings/options".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/settings/options")
})
/**
* Display the Options page.
*/
get("/:owner/:repository/settings/options")(ownerOnly {
settings.html.options(_)
settings.html.options(_, flash.get("info"))
})
/**
@@ -45,38 +46,38 @@ trait SettingsControllerBase extends ControllerBase {
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate)
redirect("/%s/%s/settings/options".format(repository.owner, repository.name))
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/options")
})
/**
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
})
/**
* JSON API for collaborator completion.
*/
get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
settings.html.collaborators(
getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
repository)
})
/**
* Add the collaborator.
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
addCollaborator(repository.owner, repository.name, form.userName)
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
addCollaborator(repository.owner, repository.name, form.userName)
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
/**
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
removeCollaborator(repository.owner, repository.name, params("name"))
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
removeCollaborator(repository.owner, repository.name, params("name"))
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
/**
@@ -96,7 +97,7 @@ trait SettingsControllerBase extends ControllerBase {
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
redirect("/%s".format(repository.owner))
redirect(s"/${repository.owner}")
})
/**
@@ -104,17 +105,12 @@ trait SettingsControllerBase extends ControllerBase {
*/
private def collaborator: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = {
val paths = request.getRequestURI.split("/")
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case Some(x) if(x.userName == context.loginAccount.get.userName) => Some("User can access this repository already.")
case Some(x) => {
val paths = request.getRequestURI.split("/")
if(getCollaborators(paths(1), paths(2)).contains(x.userName)){
Some("User can access this repository already.")
} else {
None
}
}
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName))
=> Some("User can access this repository already.")
case _ => None
}
}
}

View File

@@ -27,8 +27,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableCommitLink").toBoolean,
params("enableIssueLink").toBoolean)
params("enableRefsLink").toBoolean)
})
/**
@@ -38,61 +37,43 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(_)
})
/**
* Displays the file list of the repository root and the specified branch.
*/
get("/:owner/:repository/tree/:id")(referrersOnly {
fileList(_, params("id"))
})
/**
* Displays the file list of the specified path and branch.
*/
get("/:owner/:repository/tree/:id/*")(referrersOnly {
fileList(_, params("id"), multiParams("splat").head)
})
/**
* Displays the commit list of the specified branch.
*/
get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
val branchName = params("branch")
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30)
repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
if(path.isEmpty){
fileList(repository, id)
} else {
fileList(repository, id, path)
}
})
/**
* Displays the commit list of the specified resource.
*/
get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository =>
val branchName = params("branch")
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
val page = params.getOrElse("page", "1").toInt
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30, path)
repo.html.commits(path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
case Left(_) => NotFound
}
}
})
/**
* Displays the file content of the specified branch or commit.
*/
get("/:owner/:repository/blob/:id/*")(referrersOnly { repository =>
val id = params("id") // branch name or commit id
val raw = params.get("raw").getOrElse("false").toBoolean
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@@ -200,7 +181,17 @@ trait RepositoryViewerControllerBase extends ControllerBase {
BadRequest
}
})
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} orElse Some(path) get
(id, path.substring(id.length).replaceFirst("^/", ""))
}
/**
* Provides HTML of the file list.
*
@@ -210,40 +201,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* @return HTML of the file list
*/
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
getRepository(repository.owner, repository.name, baseUrl).map { repositoryInfo =>
val revision = if(revstr.isEmpty){
repositoryInfo.repository.defaultBranch
} else {
revstr
if(repository.commitCount == 0){
repo.html.guide(repository)
} else {
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit
revisions.map { rev => (git.getRepository.resolve(rev), rev)}.find(_._1 != null).map { case (objectId, revision) =>
val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
// get files
val files = JGitUtil.getFileList(git, revision, path)
// process README.md
val readme = files.find(_.name == "README.md").map { file =>
new String(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get, "UTF-8")
}
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme)
} getOrElse NotFound
}
JGitUtil.withGit(getRepositoryDir(repositoryInfo.owner, repositoryInfo.name)){ git =>
// get latest commit
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
val files = JGitUtil.getFileList(git, revision, path)
// process README.md
val readme = files.find(_.name == "README.md").map { file =>
new String(JGitUtil.getContent(Git.open(getRepositoryDir(repositoryInfo.owner, repositoryInfo.name)), file.id, true).get, "UTF-8")
}
repo.html.files(
// current branch
revision,
// repository
repositoryInfo,
// current path
if(path == ".") Nil else path.split("/").toList,
// latest commit
new JGitUtil.CommitInfo(revCommit),
// file list
files,
// readme
readme
)
}
} getOrElse NotFound
}
}
}

View File

@@ -0,0 +1,51 @@
package app
import util._
import service._
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService
with RepositorySearchService with IssuesService
with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService with RepositorySearchService
with ReferrerAuthenticator =>
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
post("/search", searchForm){ form =>
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
}
get("/:owner/:repository/search")(referrersOnly { repository =>
val query = params("q").trim
val target = params.getOrElse("type", "code")
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
target.toLowerCase match {
case "issue" => search.html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
})
}

View File

@@ -24,20 +24,19 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
}
post("/signin", form){ form =>
val account = getAccountByUserName(form.userName)
if(account.isEmpty || account.get.password != encrypt(form.password)){
redirect("/signin")
} else {
session.setAttribute("LOGIN_ACCOUNT", account.get)
updateLastLoginDate(account.get.userName)
getAccountByUserName(form.userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
session.setAttribute("LOGIN_ACCOUNT", account)
updateLastLoginDate(account.userName)
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/%s".format(account.get.userName))
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/")
}
}
}
} getOrElse redirect("/signin")
}
get("/signout"){

View File

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

View File

@@ -5,72 +5,135 @@ import util.AdminAuthenticator
import util.StringUtil._
import jp.sf.amateras.scalatra.forms._
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator
trait UserManagementControllerBase extends ControllerBase { self: AccountService with AdminAuthenticator =>
trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with AdminAuthenticator =>
case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean, url: Option[String])
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
val newForm = mapping(
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String], clearImage: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String])
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
)(UserNewForm.apply)
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
)(NewUserForm.apply)
val editForm = mapping(
val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
)(UserEditForm.apply)
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text())))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(EditGroupForm.apply)
get("/admin/users")(adminOnly {
admin.users.html.list(getAllUsers())
val users = getAllUsers()
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
}.toMap
admin.users.html.list(users, members)
})
get("/admin/users/_new")(adminOnly {
admin.users.html.edit(None)
get("/admin/users/_newuser")(adminOnly {
admin.users.html.user(None)
})
post("/admin/users/_new", newForm)(adminOnly { form =>
createAccount(form.userName, encrypt(form.password), form.mailAddress, form.isAdmin, form.url)
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:userName/_edit")(adminOnly {
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
admin.users.html.edit(getAccountByUserName(userName))
admin.users.html.user(getAccountByUserName(userName))
})
post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy(
password = form.password.map(encrypt).getOrElse(account.password),
password = form.password.map(sha1).getOrElse(account.password),
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
url = form.url))
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
})
// TODO Merge with AccountController?
private def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
get("/admin/users/_newgroup")(adminOnly {
admin.users.html.group(None, Nil)
})
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
val groupName = params("groupName")
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
val groupName = params("groupName")
getAccountByUserName(groupName).map { account =>
updateGroup(groupName, form.url)
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
updateGroupMembers(form.groupName, memberNames)
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
})
post("/admin/users/_usercheck")(adminOnly {
getAccountByUserName(params("userName")).isDefined
})
// TODO Merge with AccountController?
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
}

View File

@@ -1,27 +1,29 @@
package app
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil}
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
import util.Directory._
import jp.sf.amateras.scalatra.forms._
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with CollaboratorsAuthenticator with ReferrerAuthenticator
with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
self: WikiService with RepositoryService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
val newForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier, unique))),
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text()))
)(WikiPageEditForm.apply)
val editForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier))),
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required)))
@@ -30,27 +32,30 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page =>
wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect("/%s/%s/wiki/Home/_edit".format(repository.owner, repository.name))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
})
get("/:owner/:repository/wiki/:page")(referrersOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect("/%s/%s/wiki/%s/_edit".format(repository.owner, repository.name, pageName)) // TODO URLEncode
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode
})
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
wiki.html.history(Some(pageName), JGitUtil.getCommitLog(git, "master", path = pageName + ".md")._1, repository)
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
case Left(_) => NotFound
}
}
})
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
@@ -67,16 +72,20 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
})
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, context.loginAccount.get, form.message.getOrElse(""))
form.content, loginAccount, form.message.getOrElse(""))
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
})
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
@@ -84,20 +93,24 @@ trait WikiControllerBase extends ControllerBase {
})
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, context.loginAccount.get, form.message.getOrElse(""))
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
})
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, "Delete %s".format(pageName))
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, s"Delete ${pageName}")
updateLastActivityDate(repository.owner, repository.name)
redirect("/%s/%s/wiki".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/wiki")
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
@@ -107,7 +120,10 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
wiki.html.history(None, JGitUtil.getCommitLog(git, "master")._1, repository)
JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
case Left(_) => NotFound
}
}
})
@@ -123,4 +139,16 @@ trait WikiControllerBase extends ControllerBase {
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
}
private def pagename: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
}

View File

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

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

@@ -1,17 +0,0 @@
package model
import scala.slick.lifted.MappedTypeMapper
protected[model] trait Functions {
// java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
)
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}

View File

@@ -0,0 +1,14 @@
package model
import scala.slick.driver.H2Driver.simple._
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
def userName = column[String]("USER_NAME", O PrimaryKey)
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
}
case class GroupMember(
groupName: String,
userName: String
)

View File

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

View File

@@ -2,16 +2,16 @@ package model
import scala.slick.driver.H2Driver.simple._
object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate with Functions {
object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate {
def commentId = column[Int]("COMMENT_ID", O AutoInc)
def action = column[String]("ACTION")
def commentedUserName = column[String]("COMMENTED_USER_NAME")
def content = column[String]("CONTENT")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
def autoInc = userName ~ repositoryName ~ issueId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
}
@@ -20,7 +20,7 @@ case class IssueComment(
repositoryName: String,
issueId: Int,
commentId: Int,
action: Option[String],
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,

View File

@@ -2,7 +2,7 @@ package model
import scala.slick.driver.H2Driver.simple._
object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate with Functions {
object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate {
def title = column[String]("TITLE")
def description = column[String]("DESCRIPTION")
def dueDate = column[java.util.Date]("DUE_DATE")

View File

@@ -2,7 +2,7 @@ package model
import scala.slick.driver.H2Driver.simple._
object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate with Functions {
object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate {
def isPrivate = column[Boolean]("PRIVATE")
def description = column[String]("DESCRIPTION")
def defaultBranch = column[String]("DEFAULT_BRANCH")

View File

@@ -0,0 +1,20 @@
package object model {
import scala.slick.driver.BasicDriver.Implicit._
import scala.slick.lifted.{Column, MappedTypeMapper}
// java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
)
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}

View File

@@ -1,7 +1,6 @@
package service
import model._
import Accounts._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
@@ -24,7 +23,9 @@ trait AccountService {
url = url,
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None)
lastLoginDate = None,
image = None,
isGroupAccount = false)
def updateAccount(account: Account): Unit =
Accounts
@@ -38,8 +39,48 @@ trait AccountService {
account.registeredDate,
currentDate,
account.lastLoginDate)
def updateAvatarImage(userName: String, image: Option[String]): Unit =
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
def createGroup(groupName: String, url: Option[String]): Unit =
Accounts insert Account(
userName = groupName,
password = "",
mailAddress = groupName + "@devnull",
isAdmin = false,
url = url,
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
image = None,
isGroupAccount = true)
def updateGroup(groupName: String, url: Option[String]): Unit =
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
}
}
def getGroupMembers(groupName: String): List[String] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =
Query(GroupMembers)
.filter(_.userName is userName.bind)
.sortBy(_.groupName)
.map(_.groupName)
.list
}

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,8 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model._
import Issues._
import util.Implicits._
import util.StringUtil._
trait IssuesService {
import IssuesService._
@@ -36,21 +36,22 @@ trait IssuesService {
.map ( _._2 )
.list
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
/**
* Returns the count of the search result against issues.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
def countIssue(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]): Int = {
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], repos: (String, String)*): Int = {
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
(searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length
(searchIssueQuery(repos, condition, filterUser) map (_.issueId) list).length
}
/**
* Returns the Map which contains issue count for each labels.
@@ -58,14 +59,13 @@ trait IssuesService {
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @return the Map which contains issue count for each labels (key is label name, value is issue count),
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filter: String, userName: Option[String]): Map[String, Int] = {
filterUser: Map[String, String]): Map[String, Int] = {
searchIssueQuery(owner, repository, condition.copy(labels = Set.empty), filter, userName)
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -80,73 +80,93 @@ trait IssuesService {
}
.toMap
}
/**
* Returns list which contains issue count for each repository.
* If the issue does not exist, its repository is not included in the result.
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository
*/
def countIssueGroupByRepository(condition: IssueSearchCondition, filterUser: Map[String, String],
repos: (String, String)*): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser)
.groupBy { t =>
t.userName ~ t.repositoryName
}
.map { case (repo, t) =>
repo ~ t.length
}
.filter (_._3 > 0.bind)
.list
}
/**
* Returns the search result against issues.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param offset the offset for pagination
* @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count)
*/
def searchIssue(owner: String, repository: String, condition: IssueSearchCondition,
filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = {
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String],
offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
// get issues and comment count
val issues = searchIssueQuery(owner, repository, condition, filter, userName)
.leftJoin(Query(IssueComments)
.filter { _.byRepository(owner, repository) }
.groupBy { _.issueId }
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
.sortBy { case (t1, t2) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => t2._2
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
// get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.map { case (((t1, t2), t3), t4) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
}
.sortBy(_._4) // labelName
.sortBy { case (t1, commentCount, _,_,_) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => commentCount
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
}
}
}
}
.map { case (t1, t2) => (t1, t2._2.ifNull(0)) }
.drop(offset).take(limit)
.list
// get labels
val labels = Query(IssueLabels)
.innerJoin(Labels).on { (t1, t2) =>
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
}
.filter { case (t1, t2) =>
(t1.byRepository(owner, repository)) &&
(t1.issueId inSetBind (issues.map(_._1.issueId)))
}
.sortBy { case (t1, t2) => t1.issueId ~ t2.labelName }
.map { case (t1, t2) => (t1.issueId, t2) }
.list
issues.map { case (issue, commentCount) =>
(issue, labels.collect { case (issueId, labels) if(issueId == issue.issueId) => labels }, commentCount)
}
.drop(offset).take(limit)
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
.map { issues => issues.head match {
case (issue, commentCount, _,_,_) =>
(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
commentCount)
}} toList
}
/**
* Assembles query for conditional issue searching.
*/
private def searchIssueQuery(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]) =
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, filterUser: Map[String, String]) =
Query(Issues) filter { t1 =>
(t1.byRepository(owner, repository)) &&
(condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) } reduceLeft ( _ || _ ) ) &&
(t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.assignedUserName is userName.get.bind, filter == "assigned") &&
(t1.openedUserName is userName.get.bind, filter == "created_by") &&
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -189,7 +209,7 @@ trait IssuesService {
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: Option[String]) =
issueId: Int, content: String, action: String) =
IssueComments.autoInc insert (
owner,
repository,
@@ -231,10 +251,63 @@ trait IssuesService {
}
.update (closed, currentDate)
/**
* Search issues by keyword.
*
* @param owner the repository owner
* @param repository the repository name
* @param query the keywords separated by whitespace.
* @return issues with comment count and matched content of issue or comment
*/
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
import scala.slick.driver.H2Driver.likeEncode
val keywords = splitWords(query.toLowerCase)
// Search Issue
val issues = Issues
.innerJoin(IssueOutline).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.filter { case (t1, t2) =>
keywords.map { keyword =>
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
} .reduceLeft(_ && _)
}
.map { case (t1, t2) =>
(t1, 0, t1.content.?, t2.commentCount)
}
// Search IssueComment
val comments = IssueComments
.innerJoin(Issues).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
}
.filter { case ((t1, t2), t3) =>
keywords.map { query =>
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
}.reduceLeft(_ && _)
}
.map { case ((t1, t2), t3) =>
(t2, t1.commentId, t1.content.?, t3.commentCount)
}
issues.union(comments).sortBy { case (issue, commentId, _, _) =>
issue.issueId ~ commentId
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issue1.issueId == issue2.issueId
}.map { _.head match {
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
}
}.toList
}
}
object IssuesService {
import java.net.URLEncoder
import javax.servlet.http.HttpServletRequest
val IssueLimit = 30
@@ -242,12 +315,11 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None,
repo: Option[String] = None,
state: String = "open",
sort: String = "created",
direction: String = "desc"){
import IssueSearchCondition._
def toURL: String =
"?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
@@ -255,6 +327,7 @@ object IssuesService {
case Some(x) => x.toString
case None => "none"
})},
repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
@@ -263,8 +336,6 @@ object IssuesService {
object IssueSearchCondition {
private def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
val value = request.getParameter(name)
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
@@ -277,8 +348,17 @@ object IssuesService {
case "none" => None
case x => Some(x.toInt)
}),
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
}
}

View File

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

View File

@@ -0,0 +1,125 @@
package service
import model.Issue
import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._
import model.Issue
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git
trait RepositorySearchService { self: IssuesService =>
import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int =
searchIssuesByKeyword(owner, repository, query).length
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult(
issue.issueId,
issue.title,
issue.openedUserName,
issue.registeredDate,
commentCount,
getHighlightText(content, query)._1)
}
def countFiles(owner: String, repository: String, query: String): Int =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
}
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
if(JGitUtil.isEmpty(git)){
Nil
} else {
val files = searchRepositoryFiles(git, query)
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult(
path,
commits(path).getCommitterIdent.getWhen,
highlightText,
lineNumber)
}
}
}
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve("HEAD")
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.setRecursive(true)
treeWalk.addTree(revCommit.getTree)
val keywords = StringUtil.splitWords(query.toLowerCase)
val list = new ListBuffer[(String, String)]
while (treeWalk.next()) {
if(treeWalk.getFileMode(0) != FileMode.TREE){
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){
val text = new String(bytes, "UTF-8")
val lowerText = text.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
list.append((treeWalk.getPathString, text))
}
}
}
}
}
treeWalk.release
revWalk.release
list.toList
}
}
object RepositorySearchService {
val CodeLimit = 10
val IssueLimit = 10
def getHighlightText(content: String, query: String): (String, Int) = {
val keywords = StringUtil.splitWords(query.toLowerCase)
val lowerText = content.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
"<span style=\"background-color: #ffff88;;\">$1</span>")
(highlightText, lineNumber + 1)
} else {
(content.split("\n").take(5).mkString("\n"), 1)
}
}
case class SearchResult(
files : List[(String, String)],
issues: List[(Issue, Int, String)])
case class IssueSearchResult(
issueId: Int,
title: String,
openedUserName: String,
registeredDate: java.util.Date,
commentCount: Int,
highlightText: String)
case class FileSearchResult(
path: String,
lastModified: java.util.Date,
highlightText: String,
highlightLineNumber: Int)
}

View File

@@ -1,7 +1,6 @@
package service
import model._
import Repositories._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import util.JGitUtil
@@ -12,19 +11,17 @@ trait RepositoryService { self: AccountService =>
/**
* Creates a new repository.
*
* The project is created as public repository at first. Users can modify the project type at the repository settings
* page after the project creation to configure the project as the private repository.
*
* @param repositoryName the repository name
* @param userName the user name of the repository owner
* @param description the repository description
* @param isPrivate the repository type (private is true, otherwise false)
*/
def createRepository(repositoryName: String, userName: String, description: Option[String]): Unit = {
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean): Unit = {
Repositories insert
Repository(
userName = userName,
repositoryName = repositoryName,
isPrivate = false,
isPrivate = isPrivate,
description = description,
defaultBranch = "master",
registeredDate = currentDate,
@@ -35,6 +32,8 @@ trait RepositoryService { self: AccountService =>
}
def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
@@ -54,40 +53,6 @@ trait RepositoryService { self: AccountService =>
def getRepositoryNamesOfUser(userName: String): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
/**
* Returns the list of specified user's repositories information.
*
* @param userName the user name
* @param baseUrl the base url of this application
* @param loginUserName the logged in user name
* @return the list of repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(userName: String, baseUrl: String, loginUserName: Option[String]): List[RepositoryInfo] = {
val q1 = Repositories
.filter { t => t.userName is userName.bind }
.map { r => r }
val q2 = Collaborators
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter{ case (t1, t2) => t1.collaboratorName is userName.bind}
.map { case (t1, t2) => t2 }
def visibleFor(t: Repositories.type, loginUserName: Option[String]) = {
loginUserName match {
case Some(x) => (t.isPrivate is false.bind) || (
(t.isPrivate is true.bind) && ((t.userName is x.bind) || (Collaborators.filter { c =>
c.byRepository(t.userName, t.repositoryName) && (c.collaboratorName is x.bind)
}.exists)))
case None => (t.isPrivate is false.bind)
}
}
q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
}
}
/**
* Returns the specified repository information.
*
@@ -98,36 +63,48 @@ trait RepositoryService { self: AccountService =>
*/
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
/**
* Returns the list of accessible repositories information for the specified account user.
*
* @param account the account
* @param baseUrl the base url of this application
* @return the repository informations which is sorted in descending order of lastActivityDate.
* Returns the list of specified user's repositories.
* It contains own repositories and collaboration repositories.
*/
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = {
def createRepositoryInfo(repository: Repository): RepositoryInfo = {
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
Query(Repositories).filter { t1 =>
(t1.userName is userName.bind) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
(account match {
/**
* Returns the list of visible repositories for the specified user.
* If repositoryUserName is given then filters results by repository owner.
*
* @param loginAccount the logged in account
* @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @return the repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
(loginAccount match {
// for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users
case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
(Query(Collaborators).filter(t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)) exists)
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
}
// for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind)
}).sortBy(_.lastActivityDate desc).list.map(createRepositoryInfo _)
}).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
/**
@@ -165,6 +142,15 @@ trait RepositoryService { self: AccountService =>
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/**
* Remove all collaborators from the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
*/
def removeCollaborators(userName: String, repositoryName: String): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/**
* Returns the list of collaborators name which is sorted with ascending order.
*
@@ -189,6 +175,12 @@ trait RepositoryService { self: AccountService =>
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
branchList: List[String], tags: List[util.JGitUtil.TagInfo])
commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
def this(repo: JGitUtil.RepositoryInfo, model: Repository) = {
this(repo.owner, repo.name, repo.url, model, repo.commitCount, repo.branchList, repo.tags)
}
}
}

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

@@ -6,7 +6,6 @@ import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils
import util.JGitUtil.DiffInfo
import util.{Directory, JGitUtil}
import org.eclipse.jgit.lib.RepositoryBuilder
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import java.util.concurrent.ConcurrentHashMap
@@ -64,18 +63,16 @@ object WikiService {
trait WikiService {
import WikiService._
def createWikiRepository(owner: model.Account, repository: String): Unit = {
lock(owner.userName, repository){
val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = {
lock(owner, repository){
val dir = Directory.getWikiRepositoryDir(owner, repository)
if(!dir.exists){
val repo = new RepositoryBuilder().setGitDir(dir).setBare.build
try {
repo.create
saveWikiPage(owner.userName, repository, "Home", "Home", "Welcome to the %s wiki!!".format(repository), owner, "Initial Commit")
JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
} finally {
repo.close
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository))
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
}
}
}
@@ -86,14 +83,11 @@ trait WikiService {
*/
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
try {
if(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
}
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
}
} else None
}
}
@@ -102,7 +96,7 @@ trait WikiService {
*/
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
try {
if(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1)
@@ -110,10 +104,7 @@ trait WikiService {
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes
}
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
}
} else None
}
}
@@ -121,10 +112,12 @@ trait WikiService {
* Returns the list of wiki page names.
*/
def getWikiPageList(owner: String, repository: String): List[String] = {
JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
.filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x)
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x)
}
}
/**
@@ -212,12 +205,16 @@ trait WikiService {
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
if(!workDir.exists){
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
val git =
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
git.getRepository.close // close .git resources.
} else {
Git.open(workDir).pull.call
JGitUtil.withGit(workDir){ git =>
git.pull.call
}
}
}

View File

@@ -25,7 +25,7 @@ object AutoUpdate {
* If corresponding SQL file does not exist, this method do nothing.
*/
def update(conn: Connection): Unit = {
val sqlPath = "update/%d_%d.sql".format(majorVersion, minorVersion)
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
if(in != null){
val sql = IOUtils.toString(in, "UTF-8")
@@ -42,15 +42,34 @@ object AutoUpdate {
/**
* MAJOR.MINOR
*/
val versionString = "%d.%d".format(majorVersion, minorVersion)
val versionString = s"${majorVersion}.${minorVersion}"
}
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 1),
Version(1, 0)
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection): Unit = {
super.update(conn)
// Fix wiki repository configuration
val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")
while(rs.next){
val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))
val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository
val config = repository.getConfig
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
repository.close
}
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0)
)
/**
@@ -110,6 +129,7 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
}

View File

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

View File

@@ -49,7 +49,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db)
val userName = request.getAttribute("USER_NAME")
val userName = request.getAttribute("USER_NAME").asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName)
@@ -60,27 +60,52 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository))
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName))
receivePack
}
}
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService {
class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git =>
commands.asScala.foreach { command =>
JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name).foreach { commit =>
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
val issueId = matchData.group(2)
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, None)
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
val refName = command.getRefName.split("/")
// apply issue comment
val newCommits = commits.flatMap { commit =>
if(!existsCommitId(owner, repository, commit.id)){
insertCommitId(owner, repository, commit.id)
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
val issueId = matchData.group(2)
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit")
}
}
Some(commit)
} else None
}.toList
// record activity
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE => {
recordCreateBranchActivity(owner, repository, userName, refName(2))
recordPushActivity(owner, repository, userName, refName(2), newCommits)
}
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, refName(2), newCommits)
case _ =>
}
} else if(refName(1) == "tags"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits)
case _ =>
}
}
}

View File

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

View File

@@ -6,7 +6,7 @@ import javax.servlet.http.HttpServletRequest
import scala.slick.session.Database
/**
* Controls the transaction with the open session in view pattern.
* Controls the transaction with the open session in view pattern.
*/
class TransactionFilter extends Filter {
@@ -21,7 +21,6 @@ class TransactionFilter extends Filter {
// assets don't need transaction
chain.doFilter(req, res)
} else {
// TODO begin transaction!
val context = req.getServletContext
Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"),

View File

@@ -1,8 +1,6 @@
package util
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
/**
* Provides directories used by GitBucket.
@@ -13,13 +11,13 @@ object Directory {
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = "%s/repositories".format(GitBucketHome)
val RepositoryHome = s"${GitBucketHome}/repositories"
/**
* Repository names of the specified user.
*/
def getRepositories(owner: String): List[String] = {
val dir = new File("%s/%s".format(RepositoryHome, owner))
val dir = new File(s"${RepositoryHome}/${owner}")
if(dir.exists){
dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git")
@@ -33,19 +31,24 @@ object Directory {
* Substance directory of the repository.
*/
def getRepositoryDir(owner: String, repository: String): File =
new File("%s/%s/%s.git".format(RepositoryHome, owner, repository))
new File(s"${RepositoryHome}/${owner}/${repository}.git")
/**
* Directory for uploaded files by the specified user.
*/
def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
/**
* Root of temporary directories for the specified repository.
*/
def getTemporaryDir(owner: String, repository: String): File =
new File("%s/tmp/%s/%s".format(GitBucketHome, owner, repository))
new File(s"${GitBucketHome}/tmp/${owner}/${repository}")
/**
* Temporary directory which is used to create an archive to download repository contents.
*/
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
new File(getTemporaryDir(owner, repository), "download/%s".format(sessionId))
new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
/**
* Temporary directory which is used in the repository creation.
@@ -60,7 +63,7 @@ object Directory {
* Substance directory of the wiki repository.
*/
def getWikiRepositoryDir(owner: String, repository: String): File =
new File("%s/%s/%s.wiki.git".format(Directory.RepositoryHome, owner, repository))
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
/**
* Wiki working directory which is cloned from the wiki repository.

View File

@@ -1,6 +1,6 @@
package util
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
import org.apache.commons.io.{IOUtils, FileUtils}
import java.net.URLConnection
import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
@@ -44,4 +44,13 @@ object FileUtil {
}
}
def getExtension(name: String): String = {
val index = name.lastIndexOf('.')
if(index >= 0){
name.substring(index + 1)
} else {
""
}
}
}

View File

@@ -1,14 +1,13 @@
package util
import twirl.api.Html
import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex
/**
* Provides some usable implicit conversions.
*/
object Implicits {
implicit def extendsSeq[A](seq: Seq[A]) = new {
implicit class RichSeq[A](seq: Seq[A]) {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@@ -25,9 +24,23 @@ object Implicits {
}
}
// TODO Should this implicit conversion move to model.Functions?
implicit def extendsColumn(c1: Column[Boolean]) = new {
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
implicit class RichString(value: String){
def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
val sb = new StringBuilder()
var i = 0
regex.findAllIn(value).matchData.foreach { m =>
sb.append(value.substring(i, m.start))
i = m.end
replace(m) match {
case Some(s) => sb.append(s)
case None => sb.append(m.matched)
}
}
if(i < value.length){
sb.append(value.substring(i))
}
sb.toString
}
}
}

View File

@@ -14,6 +14,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.util.io.DisabledOutputStream
import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
/**
* Provides complex JGit operations.
@@ -26,10 +27,11 @@ object JGitUtil {
* @param owner the user name of the repository owner
* @param name the repository name
* @param url the repository URL
* @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001.
* @param branchList the list of branch names
* @param tags the list of tags
*/
case class RepositoryInfo(owner: String, name: String, url: String, branchList: List[String], tags: List[TagInfo])
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo])
/**
* The file data for the file list of the repository viewer.
@@ -50,14 +52,21 @@ object JGitUtil {
* @param id the commit id
* @param time the commit time
* @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message
* @param fullMessage the full message
* @param parents the list of parent commit id
*/
case class CommitInfo(id: String, time: Date, committer: String, shortMessage: String, fullMessage: String, parents: List[String]){
case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
shortMessage: String, fullMessage: String, parents: List[String]){
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
rev.getName, rev.getCommitterIdent.getWhen, rev.getCommitterIdent.getName, rev.getShortMessage, rev.getFullMessage,
rev.getName,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress,
rev.getShortMessage,
rev.getFullMessage,
rev.getParents().map(_.name).toList)
val summary = {
@@ -141,18 +150,35 @@ object JGitUtil {
*/
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
withGit(getRepositoryDir(owner, repository)){ git =>
RepositoryInfo(
owner, repository, baseUrl + "/git/%s/%s.git".format(owner, repository),
// branches
git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "")
}.toList,
// tags
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList
)
try {
// get commit count
val i = git.log.all.call.iterator
var commitCount = 0
while(i.hasNext && commitCount <= 1000){
i.next
commitCount = commitCount + 1
}
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
// commit count
commitCount,
// branches
git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "")
}.toList,
// tags
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList
)
} catch {
// not initialized
case e: NoHeadException => RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil)
}
}
}
@@ -234,7 +260,7 @@ object JGitUtil {
* @param path filters by this path. default is no filter.
* @return a tuple of the commit list and whether has next
*/
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): (List[CommitInfo], Boolean) = {
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = {
val fixedPage = if(page <= 0) 1 else page
@scala.annotation.tailrec
@@ -248,20 +274,25 @@ object JGitUtil {
}
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
val objectId = git.getRepository.resolve(revision)
if(objectId == null){
Left(s"${revision} can't be resolved.")
} else {
revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
}
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
Right(commits)
}
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
commits
}
/**
@@ -317,51 +348,11 @@ object JGitUtil {
* @return the list of latest commit
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val map = new scala.collection.mutable.HashMap[String, RevCommit]
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
//revWalk.sort(RevSort.REVERSE);
val i = revWalk.iterator
while(i.hasNext && map.size != paths.length){
val commit = i.next
if(commit.getParentCount == 0){
// Initial commit
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.reset()
treeWalk.setRecursive(true)
treeWalk.addTree(commit.getTree)
while (treeWalk.next) {
paths.foreach { path =>
if(treeWalk.getPathString.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
treeWalk.release
} else {
(0 to commit.getParentCount - 1).foreach { i =>
val parent = revWalk.parseCommit(commit.getParent(i).getId())
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
df.setRepository(git.getRepository)
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
val diffs = df.scan(parent.getTree(), commit.getTree)
diffs.asScala.foreach { diff =>
paths.foreach { path =>
if(diff.getChangeType != ChangeType.DELETE && diff.getNewPath.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
}
}
revWalk.release
}
map.toMap
val start = git.getRepository.resolve(revision)
paths.map { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
(path, commit)
}.toMap
}
/**
@@ -483,4 +474,22 @@ object JGitUtil {
}
}
}
def initRepository(dir: java.io.File): Unit = {
val repository = new RepositoryBuilder().setGitDir(dir).setBare.build
try {
repository.create
setReceivePack(repository)
} finally {
repository.close
}
}
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
}
}

View File

@@ -1,11 +1,28 @@
package util
import java.net.{URLDecoder, URLEncoder}
object StringUtil {
def encrypt(value: String): String = {
def sha1(value: String): String = {
val md = java.security.MessageDigest.getInstance("SHA-1")
md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString
}
def md5(value: String): String = {
val md = java.security.MessageDigest.getInstance("MD5")
md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString
}
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
}

View File

@@ -1,7 +1,6 @@
package util
import jp.sf.amateras.scalatra.forms._
import scala.Some
trait Validations {
@@ -11,9 +10,9 @@ trait Validations {
def identifier: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){
Some("%s contains invalid character.".format(name))
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some("%s starts with invalid character.".format(name))
Some(s"${name} starts with invalid character.")
} else {
None
}

View File

@@ -0,0 +1,36 @@
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).map { account =>
if(account.image.isEmpty){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
}
} getOrElse {
if(mailAddress.nonEmpty){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
}
}
if(tooltip){
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else {
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
}
}
}

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

View File

@@ -2,11 +2,13 @@ package view
import java.util.Date
import java.text.SimpleDateFormat
import twirl.api.Html
import util.StringUtil
import service.RequestCache
/**
* Provides helper methods for Twirl templates.
*/
object helpers {
object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
/**
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
@@ -29,39 +31,63 @@ object helpers {
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): Html = {
Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink))
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
}
/**
* Returns &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.
*/
def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String =
"%s/%s/%s".format(context.path, repository.owner, repository.name)
s"${context.path}/${repository.owner}/${repository.name}"
/**
* Generates the url to the account page.
*/
def url(userName: String)(implicit context: app.Context): String = "%s/%s".format(context.path, userName)
def url(userName: String)(implicit context: app.Context): String =
s"${context.path}/${userName}"
/**
* Returns the url to the root of assets.
*/
def assets(implicit context: app.Context): String = "%s/assets".format(context.path)
def assets(implicit context: app.Context): String =
s"${context.path}/assets"
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**
* Converts issue id and commit id to link.
* Implicit conversion to add mkHtml() to Seq[Html].
*/
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
Html(value
// escape HTML tags
.replaceAll("&", "&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 {
implicit class RichHtmlSeq(seq: Seq[Html]) {
def mkHtml(separator: String) = Html(seq.mkString(separator))
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))
}

View File

@@ -0,0 +1,6 @@
@(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, groupNames, "activity"){
@helper.html.activities(activities)
}

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

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,48 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
@if(account.url.isDefined){
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
}
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
@if(groupNames.nonEmpty){
<div>
<div>Groups</div>
@groupNames.map { groupName =>
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
}
</div>
}
</div>
<div class="span8">
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
} else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
}
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>
@body
</div>
</div>
</div>
}

View File

@@ -0,0 +1,16 @@
@(account: model.Account, members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members"){
@if(members.isEmpty){
No members
} else {
@members.map { userName =>
<div class="block">
<div class="block-header">
@avatar(userName, 20) <a href="@url(userName)">@userName</a>
</div>
</div>
}
}
}

View File

@@ -0,0 +1,25 @@
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, groupNames, "repositories"){
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
}
}
}

View File

@@ -10,6 +10,9 @@
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul>
</div>
</div>

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

View File

@@ -1,45 +0,0 @@
@(account: Option[model.Account])(implicit context: app.Context)
@import context._
@html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_new} else {@path/admin/users/@account.get.userName/_edit}" validate="true">
<fieldset>
<label for="userName"><strong>Username</strong></label>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<span id="error-userName" class="error"></span>
</fieldset>
<fieldset>
<label for="password"><strong>Password</strong>
@if(account.isDefined){
(Input to change password)
}
</label>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
<span id="error-password" class="error"></span>
</fieldset>
<fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label><strong>User Type</strong></label>
<label for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
</label>
<label for="userType_Admin">
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
</label>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
</fieldset>
<fieldset>
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}

View File

@@ -0,0 +1,121 @@
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span7">
<fieldset>
<label for="groupName"><strong>Group name</strong></label>
<span id="error-groupName" class="error"></span>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span5">
<fieldset>
<label><strong>Members</strong></label>
<ul id="members" class="collaborator">
@members.map { userName =>
<li data-name="@userName">
<a href="@path/@url(userName)">@userName</a>
<a href="#" class="remove">(remove)</a>
</li>
}
</ul>
<input type="text" id="memberName" style="width: 200px; margin-bottom: 0px;"/>
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<div>
<span class="error" id="error-memberName"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}
<script>
$(function(){
$('#memberName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#members li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
'userName': userName
}, function(data, status){
if(data == 'true'){
// add member
$('#members').append($('<li>')
.data('name', userName)
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a>').attr('href', '#').addClass('remove').text('(remove)')));
$('#memberName').val('');
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
} else {
$('#error-memberName').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
// remove member
$(this).parent().remove();
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
console.log(e.keyCode);
return !(e.keyCode == 13);
});
});
</script>

View File

@@ -1,29 +1,46 @@
@(users: List[model.Account])(implicit context: app.Context)
@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Manage Users"){
@admin.html.menu("users"){
<div style="text-align: right; margin-bottom: 4px;">
<a href="@path/admin/users/_new" class="btn">New User</a>
<a href="@path/admin/users/_newuser" class="btn">New User</a>
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
</div>
<table class="table table-bordered table-hover">
@users.map { account =>
<tr>
<td>
<div class="pull-right">
<a href="@path/admin/users/@account.userName/_edit">Edit</a>
@if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
} else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a>
}
</div>
<div class="strong">
@avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a>
@if(account.isAdmin){
(Administrator)
@if(account.isGroupAccount){
(Group)
} else {
(Normal)
@if(account.isAdmin){
(Administrator)
} else {
(Normal)
}
}
@if(account.isGroupAccount){
@members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true)
}
}
</div>
<div>
<hr>
<i class="icon-envelope"></i> @account.mailAddress
@if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress
}
@account.url.map { url =>
<i class="icon-home"></i> @url
}
@@ -31,7 +48,9 @@
<div>
<span class="muted">Registered:</span> @datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate)
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
@if(!account.isGroupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
}
</div>
</td>
</tr>

View File

@@ -0,0 +1,56 @@
@(account: Option[model.Account])(implicit context: app.Context)
@import context._
@html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
<div class="row-fluid">
<div class="span6">
<fieldset>
<label for="userName"><strong>Username</strong></label>
<span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label for="password">
<strong>Password</strong>
@if(account.isDefined){
(Input to change password)
}
</label>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset>
<fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label>
<span id="error-mailAddress" class="error"></span>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
</fieldset>
<fieldset>
<label><strong>User Type</strong></label>
<label for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
</label>
<label for="userType_Admin">
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
</label>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}

View File

@@ -0,0 +1,48 @@
@(listparts: twirl.api.Html,
allCount: Int,
assignedCount: Int,
createdByCount: Int,
repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Your Issues"){
@dashboard.html.tab("issues")
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@path/dashboard/issues/repos@condition.toURL">
<span class="count-right">@allCount</span>
In your repositories
</a>
</li>
<li@if(filter == "assigned"){ class="active"}>
<a href="@path/dashboard/issues/assigned@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/issues/created_by@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
</div>
}

View File

@@ -0,0 +1,8 @@
@(active: String = "")(implicit context: app.Context)
@import context._
<ul class="nav nav-tabs">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
@if(loginAccount.isDefined){
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
}
</ul>

View File

@@ -0,0 +1,45 @@
@(activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
@if(activities.isEmpty){
No activity
} else {
@activities.map { activity =>
<div class="block">
<div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
@if(additionalInfo.nonEmpty){
@(activity.activityType match {
case "create_wiki" => {
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "edit_wiki" => {
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "push" => {
<div class="small activity-message">
{additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
<div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}}
</div>
}
case _ => {
<div class=" activity-message">{additionalInfo}</div>
}
})
}
}
</div>
}
}

View File

@@ -1,7 +1,11 @@
@(body: Html)
@(buttonValue: String = "")(body: Html)
<div class="btn-group">
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown">
<i class="icon-cog"></i>
@if(buttonValue == ""){
<i class="icon-cog"></i>
} else {
<strong>@buttonValue</strong>
}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">

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,32 +1,32 @@
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String)
@defining(view.Pagination(page, count, service.IssuesService.IssueLimit, width)){ p =>
@defining(view.Pagination(page, count, limit, width)){ p =>
@if(p.count > p.limit){
<div class="pagination">
<ul>
@if(page == 1){
<li class="disabled"><span>&#9664;</span></li>
<li class="disabled"><span>&#9664;</span></li>
} else {
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li>
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li>
}
@for(i <- 1 to p.max){
@if(i == p.max && p.omitRight){
<li><span>&hellip;</span></li>
}
@if(i == page){
<li class="active"><span>@i</span></li>
} else {
@if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li>
}
}
@if(i == 1 && p.omitLeft){
<li><span>&hellip;</span></li>
}
@if(i == p.max && p.omitRight){
<li><span>&hellip;</span></li>
}
@if(i == page){
<li class="active"><span>@i</span></li>
} else {
@if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li>
}
}
@if(i == 1 && p.omitLeft){
<li><span>&hellip;</span></li>
}
}
@if(page == p.max){
<li class="disabled"><span>&#9654;</span></li>
<li class="disabled"><span>&#9654;</span></li>
} else {
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li>
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li>
}
</ul>
</div>

View File

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

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,
userRepositories: List[String])(implicit context: app.Context)
@(activities: List[model.Activity],
recentRepositories: List[service.RepositoryService.RepositoryInfo],
systemSettings: service.SystemSettingsService.SystemSettings,
userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@main("GitBucket"){
@dashboard.html.tab()
<div class="row-fluid">
<div class="span8">
<h3>Recent updated repositories</h3>
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
}
@helper.html.activities(activities)
</div>
<div class="span4">
@if(loginAccount.isEmpty){
@@ -29,15 +16,53 @@
} else {
<table class="table table-bordered">
<tr>
<th class="metal">Your repositories (@userRepositories.size)</th>
<th class="metal">
<div class="pull-right">
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
</div>
Your repositories (@userRepositories.size)
</th>
</tr>
@userRepositories.map { repositoryName =>
@if(userRepositories.isEmpty){
<tr>
<td><a href="@path/@loginAccount.get.userName/@repositoryName">@repositoryName</a></td>
<td>No repositories</td>
</tr>
} else {
@userRepositories.map { repository =>
<tr>
<td>
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><strong>@repository.name</strong></a>
} else {
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
}
</td>
</tr>
}
}
</table>
}
<table class="table table-bordered">
<tr>
<th class="metal">
Recent updated repositories
</th>
</tr>
@if(recentRepositories.isEmpty){
<tr>
<td>No repositories</td>
</tr>
} else {
@recentRepositories.map { repository =>
<tr>
<td>
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
</td>
</tr>
}
}
</table>
</div>
</div>

View File

@@ -5,25 +5,25 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("New Issue - " + repository.owner + "/" + repository.name){
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("", repository)
<form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid">
<div class="span9">
<div class="box">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box">
<div class="box-content">
<span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 650px;"/>
<input type="text" name="title" value="" placeholder="Title" style="width: 600px;"/>
<div>
<span id="label-assigned">No one is assigned</span>
@if(hasWritePermission){
<input type="hidden" name="assignedUserName" value=""/>
@helper.html.dropdown {
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i> @collaborator</a></li>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
}
@@ -31,21 +31,37 @@
<span id="label-milestone">No milestone</span>
@if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown {
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId"><i class="icon-while"></i> @milestone.title</a></li>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-while"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
</div>
</div>
<hr>
@helper.html.preview(repository, "", false, true, true, "width: 650px; height: 200px;")
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
</div>
</div>
<input type="submit" class="btn btn-success" value="Submit new issue"/>
<div class="pull-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/>
</div>
</div>
<div class="span3">
@if(hasWritePermission){
@@ -88,7 +104,7 @@ $(function(){
});
$('a.milestone').click(function(){
var title = $(this).text();
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$('a.milestone i.icon-ok').attr('class', 'icon-white');

View File

@@ -1,7 +1,7 @@
@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context)
@import context._
<span id="error-edit-content-@commentId" class="error"></span>
<textarea style="width: 730px; height: 100px;" id="edit-content-@commentId">@content</textarea>
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
<input type="button" class="btn btn-small" value="Update Comment"/>
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
<script>

View File

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

View File

@@ -2,13 +2,13 @@
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[model.Milestone],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("%s - Issue #%d - %s/%s".format(issue.title, issue.issueId, repository.owner, repository.name)){
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("issues", repository)
<ul class="nav nav-tabs">
@@ -17,7 +17,8 @@
</ul>
<div class="row-fluid">
<div class="span10">
<div class="box">
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-box">
<div class="box-content" style="padding: 0px;">
<div class="issue-header">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
@@ -31,82 +32,115 @@
<div class="issue-info">
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
<a href="@url(userName)" class="username strong">@userName</a> is assigned
@avatar(userName, 20) <a href="@url(userName)" class="username strong">@userName</a> is assigned
}.getOrElse("No one is assigned")
</span>
@if(hasWritePermission){
@helper.html.dropdown {
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i> @collaborator</a></li>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
}
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.find(_.milestoneId == milestoneId).map { milestone =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <strong>@milestone.title</strong>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown {
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId"><i class="icon-white"></i> @milestone.title</a></li>
@milestones.map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
</div>
</div>
<div class="issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description given.", repository, false, true, true)
@markdown(issue.content getOrElse "No description given.", repository, false, true)
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<strong>@participants.size</strong> @plural(participants.size, "participant")
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
}
</div>
@comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small">
<i class="icon-comment"></i>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented
<span class="pull-right">
@datetime(comment.registeredDate)
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
@if(comment.action != "commit" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
}
</span>
</div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
@markdown(comment.content, repository, false, true, true)
@markdown(comment.content, repository, false, true)
</div>
</div>
@comment.action.map { action =>
}
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="small issue-comment-action">
@if(action == "close"){
<span class="label label-important">Closed</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
} else {
<span class="label label-success">Reopened</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
}
<span class="label label-important">Closed</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
</div>
}
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
<div class="small issue-comment-action">
<span class="label label-success">Reopened</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
</div>
}
}
@if(loginAccount.isDefined){
<form action="@url(repository)/issue_comments/new" method="POST" validate="true">
<div class="box">
<form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">
<div class="box-content">
@helper.html.preview(repository, "", false, true, true, "width: 730px; height: 100px;")
@helper.html.preview(repository, "", false, true, "width: 680px; height: 100px;")
</div>
</div>
<input type="hidden" name="issueId" value="@issue.issueId"/>
<input type="submit" class="btn btn-success" value="Comment"/>
@if(hasWritePermission || issue.openedUserName == loginAccount.get.userName){
<input type="submit" class="btn" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
<div class="pull-right">
<input type="hidden" name="issueId" value="@issue.issueId"/>
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
@if(hasWritePermission || issue.openedUserName == loginAccount.get.userName){
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
</div>
</form>
}
</div>
@@ -117,14 +151,16 @@
<span class="label label-success issue-status">Open</span>
}
<div class="small" style="text-align: center;">
<strong>@comments.size</strong> @plural(comments.size, "comment")
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
<strong>@count</strong> @plural(count, "comment")
}
</div>
<hr/>
<div style="margin-bottom: 8px;">
<strong>Labels</strong>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown {
@helper.html.dropdown() {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@@ -134,7 +170,6 @@
</a>
</li>
}
</ul>
}
</div>
}
@@ -166,7 +201,8 @@ $(function(){
});
$('a.assign').click(function(){
var userName = $(this).data('name');
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
@@ -176,27 +212,31 @@ $(function(){
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').html($('<span>')
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned'));
.append(' is assigned');
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).text();
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(){
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});

View File

@@ -1,7 +1,8 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
page: Int,
labels: List[model.Label],
collaborators: List[String],
milestones: List[model.Milestone],
labels: List[model.Label],
openCount: Int,
closedCount: Int,
allCount: Int,
@@ -14,7 +15,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Issues - " + repository.owner + "/" + repository.name){
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("issues", repository)
<div class="row-fluid">
@@ -51,7 +52,7 @@
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
}
}
@helper.html.dropdown {
@helper.html.dropdown() {
@if(condition.milestoneId.isDefined){
<li>
<a href="@condition.copy(milestoneId = None).toURL">
@@ -64,21 +65,45 @@
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
</a>
</li>
@milestones.map { milestone =>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
</a>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
<div class="milestone-progress" style="margin-top: 8px;">
@if(closedCount > 0){
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
<div style="margin-top: 4px;">
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
</div>
<span class="muted small">@openCount open issues</span>
@if(milestone.closedDate.isDefined){
@milestone.closedDate.map { closedDate =>
<span class="small">Closed in @date(closedDate)</span>
}
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="small">Due in @date(dueDate)</span>
}
}
}
</div>
<span class="muted small">@openCount open issues</span>
}
}
<hr/>
<strong>Labels</strong>
@@ -106,101 +131,15 @@
@_root_.issues.labels.html.edit(None, repository)
}
</div>
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</div>
<div class="btn-group">
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
<div class="btn-group">
<button class="btn dropdown-toggle" data-toggle="dropdown">
Sort:
<strong>
@if(condition.sort == "created" && condition.direction == "desc"){ Newest }
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest }
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented }
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented }
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated }
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated }
</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
</ul>
</div>
<table class="table table-bordered table-hover table-issues">
@issues.map { case (issue, labels, commentCount) =>
<tr>
<td>
<a href="@url(repository)/issues/@issue.issueId" class="issue-title">@issue.title</a>
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">#@issue.issueId</span>
<div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)&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){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
<a href="@url(repository)/issues/new">Create a new issue.</a>
}
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>
</div>
@***** show issue list *****@
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
</div>
@if(hasWritePermission){
<form id="batcheditForm" method="POST">
<input type="hidden" name="value"/>
<input type="hidden" name="checked"/>
</form>
}
}
@if(hasWritePermission){
<script>
@@ -215,6 +154,33 @@ $(function(){
});
}
});
$('.table-issues input[type=checkbox]').change(function(){
$('.table-issues button').prop('disabled',
!$('.table-issues input[type=checkbox]').filter(':checked').length);
}).filter(':first').change();
var submitBatchEdit = function(action, value) {
var checked = $('.table-issues input[type=checkbox]').filter(':checked').map(function(){ return this.value; }).get().join();
var form = $('#batcheditForm');
form.find('input[name=value]').val(value);
form.find('input[name=checked]').val(checked);
form.attr('action', action);
form.submit();
};
$('#state').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase());
});
$('a.toggle-label').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id'));
});
$('a.toggle-assign').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/assign', $(this).data('name'));
});
$('a.toggle-milestone').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/milestone', $(this).data('id'));
});
});
</script>
}

View File

@@ -0,0 +1,176 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
collaborators: List[String] = Nil,
milestones: List[model.Milestone] = Nil,
labels: List[model.Label] = Nil,
repository: Option[service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
@if(condition.repo.isDefined){
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</div>
<div class="btn-group">
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
<div class="btn-group">
<button class="btn dropdown-toggle" data-toggle="dropdown">
Sort:
<strong>
@if(condition.sort == "created" && condition.direction == "desc"){ Newest }
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest }
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented }
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented }
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated }
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated }
</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
</ul>
</div>
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini" id="state"><strong>@{if(condition.state == "open") "Close" else "Reopen"}</strong></button>
</div>
@helper.html.dropdown("Label") {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&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>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.map { milestone =>
<li>
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</td>
</tr>
}
}
@issues.map { case (issue, labels, commentCount) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
#@issue.issueId
</span>
<div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
</label>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>
</div>

View File

@@ -1,7 +1,7 @@
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository)
@issues.html.tab("milestones", repository)
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">

View File

@@ -4,7 +4,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository)
@issues.html.tab("milestones", repository)
<div class="row-fluid">
@@ -42,9 +42,13 @@
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
} else {
@if(milestone.dueDate.isDefined){
<span class="muted">Due in @date(milestone.dueDate.get)</span>
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
}
@@ -65,23 +69,12 @@
</div>
<span class="muted">@closedCount closed - @openCount open</span>
</div>
<div class="milestone-progress">
@if(closedCount > 0){
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
}
<span class="milestone-percentage">
@if(closedCount == 0){
0%
} else {
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
}
</span>
</div>
@progress(openCount + closedCount, closedCount, true)
</div>
</div>
@if(milestone.description.isDefined){
<div class="milestone-description">
@markdown(milestone.description.get, repository, false, false, false)
@markdown(milestone.description.get, repository, false, false)
</div>
}
</td>

View File

@@ -0,0 +1,15 @@
@(total: Int, progress: Int, showPercentage: Boolean)
<div class="milestone-progress">
@if(progress > 0){
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
}
@if(showPercentage){
<span class="milestone-percentage">
@if(progress == 0){
0%
} else {
@((progress.toDouble / total.toDouble * 100).toInt)%
}
</span>
}
</div>

View File

@@ -1,4 +1,4 @@
@(title: String)(body: Html)(implicit context: app.Context)
@(title: String, repository: Option[service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
<!DOCTYPE html>
@@ -19,42 +19,60 @@
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/common/js/jquery-1.9.1.js"></script>
<script src="@assets/common/js/dropzone.js"></script>
<script src="@assets/common/js/validation.js"></script>
<script src="@assets/common/js/gitbucket.js"></script>
<script src="@assets/bootstrap/js/bootstrap.js"></script>
<script src="@assets/datepicker/js/bootstrap-datepicker.js"></script>
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/google-code-prettify/prettify.js"></script>
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
</head>
<body>
<div class="navbar navbar-inverse">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">GitBucket</a>
<div class="nav-collapse collapse">
<ul class="nav pull-right">
@if(loginAccount.isDefined){
<li><a href="@path/new">New repo</a></li>
<li><a href="@url(loginAccount.get.userName)">Account</a></li>
@if(loginAccount.get.isAdmin){
<li><a href="@path/admin/users">Administration</a></li>
}
<li><a href="@path/signout">Sign out</a></li>
} else {
<li><a href="@path/signin?@currentUrl">Sign in</a></li>
<form id="search" action="@path/search" method="POST">
<div class="navbar">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">GitBucket</a>
<div class="nav-collapse collapse pull-right header-menu">
@repository.map { repository =>
<input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
</ul>
</div><!--/.nav-collapse -->
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>
</div>
</div>
</form>
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
}
</div>
<div class="container body">
@body
</div>
<script>
$(function(){
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
});
</script>
</body>
</html>
</html>

View File

@@ -1,18 +1,73 @@
@()(implicit context: app.Context)
@(groupNames: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main("Create a New Repository"){
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name"><strong>Repository name</strong></label>
<input type="text" name="name" id="name" />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong> (optional)</label>
<input type="text" name="description" id="description" style="width: 600px;"/>
</fieldset>
<fieldset>
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
</form>
}
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name"><strong>Repository name</strong></label>
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
<button class="btn dropdown-toggle" data-toggle="dropdown">
<strong>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="icon-ok"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
@groupNames.map { groupName =>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
}
</ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
</div>
/
<input type="text" name="name" id="name" />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong> (optional)</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label for="createReadme">
<input type="checkbox" name="createReadme" id="createReadme"/>
<strong>Initialize this repository with a README</strong>
<div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
</form>
</div>
}
<script>
$('#owner-dropdown a').click(function(){
var userName = $(this).data('name');
$('#owner').val(userName);
$('#owner-dropdown i').attr('class', 'icon-white');
$(this).find('i').attr('class', 'icon-ok');
$('#owner-dropdown strong').html($(this).find('span').html());
});
</script>

View File

@@ -5,7 +5,7 @@
latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner+"/"+repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<div class="head">
@@ -23,6 +23,7 @@
<tr>
<th style="font-weight: normal;">
<div class="pull-left">
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
<span class="muted">@datetime(latestCommit.time)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>

View File

@@ -8,7 +8,7 @@
@import view.helpers._
@import util.Implicits._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main(commit.shortMessage){
@html.main(commit.shortMessage, Some(repository)){
@html.header("code", repository)
@tab(commitId, repository, "commits")
<table class="table table-bordered">
@@ -43,7 +43,9 @@
</tr>
<tr>
<td>
<a href="@url(commit.committer)" class="username strong">@commit.committer</a> <span class="muted">@datetime(commit.time)</span>
@avatar(commit, 20)
<a href="@url(commit.committer)" class="username strong">@commit.committer</a>
<span class="muted">@datetime(commit.time)</span>
<div class="pull-right monospace small" style="text-align: right;">
<div>
@if(commit.parents.size == 0){
@@ -72,11 +74,14 @@
<div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div>
Showing @diffs.size changed @plural(diffs.size, "file")
@*
@if(diffs.size == 1){
Showing 1 changed files
Showing 1 changed file
} else {
Showing @diffs.size changed files
}
*@
</div>
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>

View File

@@ -6,7 +6,7 @@
hasNext: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner+"/"+repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, if(pathList.isEmpty) "commits" else "files")
<div class="head">
@@ -33,24 +33,27 @@
@day.map { commit =>
<tr>
<td>
<div class="pull-left">
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
}
<br>
@if(commit.description.isDefined){
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
}
<div class="small">
<a href="@url(commit.committer)" class="username">@commit.committer</a>
<span class="muted">@datetime(commit.time)</span>
</div>
</div>
<div class="pull-right align-right">
<a href="@url(repository)/commit/@commit.id" class="btn btn-small monospace">@commit.id.substring(0, 10)</a><br>
<a href="@url(repository)/tree/@commit.id" class="small">Browse code</a>
</div>
<div>
<div class="commit-avatar-image">@avatar(commit, 40)</div>
<div class="commit-message-box">
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
}
<br>
@if(commit.description.isDefined){
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
}
<div class="small">
<a href="@url(commit.committer)" class="username">@commit.committer</a>
<span class="muted">@datetime(commit.time)</span>
</div>
</div>
</div>
</td>
</tr>
}

View File

@@ -6,10 +6,15 @@
readme: Option[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<div class="head">
<div class="pull-right">
@defining(repository.commitCount){ commitCount =>
<a href="@url(repository)/commits/@branch">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
}
</div>
<a href="@url(repository)/tree/@branch">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@url(repository)/tree/@branch/@pathList.take(i + 1).mkString("/")">@section</a> /
@@ -22,8 +27,6 @@
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
@if(latestCommit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@latestCommit.id').toggle();" class="omit">...</a>
}
@if(latestCommit.description.isDefined){
<pre id="description-@latestCommit.id" class="commit-description" style="display: none;">@link(latestCommit.description.get, repository)</pre>
}
</th>
@@ -31,6 +34,7 @@
<tr>
<td colspan="4" class="latest-commit">
<div>
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
<span class="muted">@datetime(latestCommit.time)</span>
<div class="pull-right align-right monospace">
@@ -73,7 +77,7 @@
@readme.map { content =>
<div class="box">
<div class="box-header">README.md</div>
<div class="box-content">@markdown(content, repository, false, false, false)</div>
<div class="box-content">@markdown(content, repository, false, false)</div>
</div>
}
}

View File

@@ -0,0 +1,21 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
<pre>
touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin @repository.url
git push -u origin master
</pre>
<h3 style="margin-top: 30px;">Push an existing repository from the command line</h3>
<pre>
git remote add origin @repository.url
git push -u origin master
</pre>
}

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

View File

@@ -1,9 +1,9 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab("master", repository, "tags") @* TODO DON'T display branch pulldown *@
@tab(repository.repository.defaultBranch, repository, "tags", true)
<h1>Tags</h1>
<table class="table table-bordered">
<tr>

View File

@@ -0,0 +1,26 @@
@(files: List[service.RepositorySearchService.FileSearchResult],
issueCount: Int,
query: String,
page: Int,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.RepositorySearchService._
@html.main("Search Results", Some(repository)){
@menu("code", files.size, issueCount, query, repository){
@if(files.isEmpty){
<h4>We couldn't find any code matching '@query'</h4>
} else {
<h4>We've found @files.size code @plural(files.size, "result")</h4>
}
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
<div>
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
<div class="small muted">Latest commit at @datetime(file.lastModified)</div>
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
</div>
}
@helper.html.paginator(page, files.size, CodeLimit, 10,
s"${url(repository)}/search?q=${urlEncode(query)}&type=code")
}
}

View File

@@ -0,0 +1,35 @@
@(issues: List[service.RepositorySearchService.IssueSearchResult],
fileCount: Int,
query: String,
page: Int,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.RepositorySearchService._
@html.main("Search Results", Some(repository)){
@menu("issue", fileCount, issues.size, query, repository){
@if(issues.isEmpty){
<h4>We couldn't find any code matching '@query'</h4>
} else {
<h4>We've found @issues.size code @plural(issues.size, "result")</h4>
}
@issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue =>
<div class="block">
<div class="pull-right muted">#@issue.issueId</div>
<h4 style="margin-top: 0px;"><a href="@url(repository)/issues/@issue.issueId">@issue.title</a></h4>
@if(issue.highlightText.nonEmpty){
<pre>@Html(issue.highlightText)</pre>
}
<div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
at @datetime(issue.registeredDate)
@if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="icon-comment"></i><strong>@issue.commentCount</strong> @plural(issue.commentCount, "comment")
}
</div>
</div>
}
@helper.html.paginator(page, issues.size, IssueLimit, 10,
s"${url(repository)}/search?q=${urlEncode(query)}&type=issue")
}
}

View File

@@ -0,0 +1,37 @@
@(active: String, fileCount: Int, issueCount: Int, query: String,
repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.header("", repository)
<div class="row-fluid">
<div class="span3">
<div class="box">
<ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="code"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=code">
@if(fileCount != 0){
<span class="badge pull-right">@fileCount</span>
}
Code
</a>
</li>
<li@if(active=="issue"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=issue">
@if(issueCount != 0){
<span class="badge pull-right">@issueCount</span>
}
Issue
</a>
</li>
</ul>
</div>
</div>
<div class="span9">
<form action="@url(repository)/search" method="GET">
<input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/>
<input type="submit" value="Search" class="btn" style="width: 15%;"/>
<input type="hidden" name="type" value="@active"/>
</form>
@body
</div>
</div>

View File

@@ -1,7 +1,9 @@
@(collaborators: List[String], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@(collaborators: List[String],
isGroupRepository: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Settings"){
@html.main("Settings", Some(repository)){
@html.header("settings", repository)
@menu("collaborators", repository){
<h3>Manage Collaborators</h3>
@@ -9,26 +11,32 @@
@collaborators.map { collaboratorName =>
<li>
<a href="@url(collaboratorName)">@collaboratorName</a>
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
@if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
}
</li>
}
</ul>
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
<div>
<span class="error" id="error-userName"></span>
</div>
<input type="text" name="userName" id="userName" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
</form>
@if(!isGroupRepository){
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
<div>
<span class="error" id="error-userName"></span>
</div>
<input type="text" name="userName" id="userName" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
</form>
}
}
}
<script>
$('#userName').typeahead({
source: function (query, process) {
return $.get('@url(repository)/settings/collaborators/proposals', { query: query },
function (data) {
return process(data.options);
});
}
$(function(){
$('#userName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
});
</script>

Some files were not shown because too many files have changed in this diff Show More