Compare commits

...

216 Commits
1.0 ... 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
takezoe
bdd83a84fd (refs #20)Upgrade to JGit 3.0.0. 2013-07-05 11:10:44 +09:00
takezoe
ef38855b4b Selected label style is changed to bold. 2013-07-05 04:20:41 +09:00
takezoe
9bc8db5a15 Disable GET Ajax cache. 2013-07-05 02:35:23 +09:00
takezoe
c53f3843b8 (refs #19)Add unique checking for mail address. 2013-07-05 01:37:29 +09:00
takezoe
56f1f5d47f Remove all error messages before validation. 2013-07-04 22:53:50 +09:00
takezoe
d74ef599d3 Fix redirect path. 2013-07-04 22:45:03 +09:00
takezoe
398c77e277 Fix form action of the account register form. 2013-07-04 22:43:16 +09:00
takezoe
99e562e9e6 Fix redirect path after milestone updating. 2013-07-04 22:42:31 +09:00
takezoe
47bdb8da23 (refs #18)Add schema update to fix constraints for COLLABORATOR. 2013-07-04 18:46:01 +09:00
takezoe
869930165c (refs #17)Fix wiki link. 2013-07-04 17:26:32 +09:00
takezoe
d0f052e056 Hide comment count for no comment issues. 2013-07-04 16:45:47 +09:00
takezoe
afd2325678 (refs #16)Fixed foreign key constraint problem in repository deletion. 2013-07-04 16:06:55 +09:00
120 changed files with 5370 additions and 1171 deletions

4
.gitignore vendored
View File

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

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,7 +1,7 @@
GitBucket GitBucket
========= =========
GitBucket is a Github clone by Scala, Easy to setup. GitBucket is the easily installable Github clone written with Scala.
The current version of GitBucket provides a basic features below: The current version of GitBucket provides a basic features below:
@@ -9,12 +9,12 @@ The current version of GitBucket provides a basic features below:
- Repository viewer (some advanced features are not implemented) - Repository viewer (some advanced features are not implemented)
- Wiki - Wiki
- Issues - Issues
- Activity timeline
- User management (for Administrators) - User management (for Administrators)
Following features are not implemented, but we will make them in the future release! Following features are not implemented, but we will make them in the future release!
- Fork and pull request - Fork and pull request
- Timeline
- Search - Search
- Network graph - Network graph
- Statics - Statics
@@ -32,9 +32,39 @@ Installation
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
To upgrade GitBucket, only replace gitbucket.war.
Release Notes Release Notes
-------- --------
### 1.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 ### 1.0 - 04 Jul 2013
- This is a first public release. - This is a first public release.

View File

@@ -22,7 +22,7 @@ object MyBuild extends Build {
scalaVersion := ScalaVersion, scalaVersion := ScalaVersion,
resolvers += Classpaths.typesafeReleases, resolvers += Classpaths.typesafeReleases,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "2.3.1.201302201838-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.apache.commons" % "commons-io" % "1.3.2", "org.apache.commons" % "commons-io" % "1.3.2",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0 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,8 @@
-- Fix COLLABORATOR constraints
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK1 IF EXISTS;
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK0 IF EXISTS;
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_PK IF EXISTS;
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COLLABORATOR_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);

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

View File

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

View File

@@ -1,24 +1,58 @@
package app package app
import model.Account import _root_.util.Directory._
import util.Validations import _root_.util.{StringUtil, FileUtil, Validations}
import org.scalatra._ import org.scalatra._
import org.scalatra.json._ import org.scalatra.json._
import org.json4s._ import org.json4s._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model.Account
import scala.Some
import service.AccountService
import javax.servlet.http.{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 abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations { with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
implicit val jsonFormats = DefaultFormats 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. * Returns the context object for the request.
*/ */
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL) implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
private def currentURL: String = { private def currentURL: String = {
val queryString = request.getQueryString val queryString = request.getQueryString
@@ -87,4 +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 package app
import util.Directory._ import util.Directory._
import util.UsersAuthenticator import util.{JGitUtil, UsersAuthenticator}
import service._ import service._
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.apache.commons.io._ import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class CreateRepositoryController extends CreateRepositoryControllerBase class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with UsersAuthenticator with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator
/** /**
* Creates new repository. * Creates new repository.
*/ */
trait CreateRepositoryControllerBase extends ControllerBase { trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with WikiService with LabelsService with UsersAuthenticator => self: RepositoryService with 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( val form = mapping(
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))), "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"description" -> trim(label("Description" , optional(text()))) "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) )(RepositoryCreationForm.apply)
/** /**
* Show the new repository form. * Show the new repository form.
*/ */
get("/new")(usersOnly { get("/new")(usersOnly {
html.newrepo() html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
}) })
/** /**
* Create new repository. * Create new repository.
*/ */
post("/new", form)(usersOnly { form => 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 // 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 // Insert default labels
createLabel(loginUserName, form.name, "bug", "fc2929") createLabel(form.owner, form.name, "bug", "fc2929")
createLabel(loginUserName, form.name, "duplicate", "cccccc") createLabel(form.owner, form.name, "duplicate", "cccccc")
createLabel(loginUserName, form.name, "enhancement", "84b6eb") createLabel(form.owner, form.name, "enhancement", "84b6eb")
createLabel(loginUserName, form.name, "invalid", "e6e6e6") createLabel(form.owner, form.name, "invalid", "e6e6e6")
createLabel(loginUserName, form.name, "question", "cc317c") createLabel(form.owner, form.name, "question", "cc317c")
createLabel(loginUserName, form.name, "wontfix", "ffffff") createLabel(form.owner, form.name, "wontfix", "ffffff")
// Create the actual repository // Create the actual repository
val gitdir = getRepositoryDir(loginUserName, form.name) val gitdir = getRepositoryDir(form.owner, form.name)
val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build 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 // Create README.md
config.setBoolean("http", null, "receivepack", true) FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
config.save if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}, "UTF-8")
val tmpdir = getInitRepositoryDir(loginUserName, form.name) val git = Git.open(tmpdir)
try { git.add.addFilepattern("README.md").call
// Clone the repository git.commit.setCommitter(loginAccount.userName, loginAccount.mailAddress).setMessage("Initial commit").call
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call git.push.call
// Create README.md } finally {
FileUtils.writeStringToFile(new File(tmpdir, "README.md"), FileUtils.deleteDirectory(tmpdir)
if(form.description.nonEmpty){ }
form.name + "\n===============\n\n" + form.description.get
} else {
form.name + "\n===============\n"
}, "UTF-8")
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit.setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
} }
// Create Wiki repository // Create Wiki repository
createWikiRepository(context.loginAccount.get, form.name) createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
// redirect to the repository // 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. * Duplicate check for the repository name.
*/ */
private def unique: Constraint = new Constraint(){ private def unique: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = 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 package app
import util._
import service._ 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("/"){ get("/"){
html.index(getAccessibleRepositories(context.loginAccount, baseUrl), loadSystemSettings(), val loginAccount = context.loginAccount
context.loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil))
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 import org.scalatra.Ok
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with LabelsService with MilestonesService self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String]) case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
val issueCreateForm = mapping( val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
@@ -40,6 +39,11 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(label("Comment", text(required))) "content" -> trim(label("Comment", text(required)))
)(CommentForm.apply) )(CommentForm.apply)
val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _) searchIssues("all", _)
}) })
@@ -63,7 +67,7 @@ trait IssuesControllerBase extends ControllerBase {
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted, (getCollaborators(owner, name) :+ owner).sorted,
getMilestones(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
repository) repository)
@@ -86,11 +90,14 @@ trait IssuesControllerBase extends ControllerBase {
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
val writable = hasWritePermission(owner, name, context.loginAccount) val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName
val issueId = createIssue(owner, name, context.loginAccount.get.userName, form.title, form.content, // insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None, if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None) if(writable) form.milestoneId else None)
// insert labels
if(writable){ if(writable){
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
@@ -102,7 +109,10 @@ trait IssuesControllerBase extends ControllerBase {
} }
} }
redirect("/%s/%s/issues/%d".format(owner, name, issueId)) // record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
redirect(s"/${owner}/${name}/issues/${issueId}")
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
@@ -112,28 +122,21 @@ trait IssuesControllerBase extends ControllerBase {
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditable(owner, name, issue.openedUserName)){
updateIssue(owner, name, issue.issueId, form.title, form.content) updateIssue(owner, name, issue.issueId, form.title, form.content)
redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId)) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner handleComment(form.issueId, Some(form.content), repository)() map { id =>
val name = repository.name redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
getIssue(owner, name, form.issueId.toString).map { issue => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
redirect("/%s/%s/issues/%d#comment-%d".format( handleComment(form.issueId, form.content, repository)() map { id =>
owner, name, form.issueId, redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
createComment(owner, name, context.loginAccount.get.userName, } getOrElse NotFound
form.issueId,
form.content,
if(isEditable(owner, name, issue.openedUserName)){
params.get("action") filter { action =>
updateClosed(owner, name, form.issueId, if(action == "close") true else false) > 0
}
} else None)
))
}
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
@@ -143,7 +146,7 @@ trait IssuesControllerBase extends ControllerBase {
getComment(owner, name, params("id")).map { comment => getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content) updateComment(comment.commentId, form.content)
redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId)) redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
@@ -159,7 +162,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("title" -> x.title, Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
@@ -176,7 +179,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content, Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
@@ -198,32 +201,109 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
params.get("assignedUserName") filter (_.trim != ""))
Ok("updated") Ok("updated")
}) })
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
params.get("milestoneId") collect { case x if x.trim != "" => x.toInt }) milestoneId("milestoneId").map { milestoneId =>
Ok("updated") 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 = private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues")
}
/**
* @see [[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) = { private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner val owner = repository.owner
val repoName = repository.name val repoName = repository.name
val userName = if(filter != "all") Some(params("userName")) else None val filterUser = Map(filter -> params.getOrElse("userName", ""))
val sessionKey = "%s/%s/issues".format(owner, repoName) val page = IssueSearchCondition.page(request)
val sessionKey = s"${owner}/${repoName}/issues"
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
// retrieve search condition // retrieve search condition
val condition = if(request.getQueryString == null){ val condition = if(request.getQueryString == null){
@@ -233,16 +313,17 @@ trait IssuesControllerBase extends ControllerBase {
session.put(sessionKey, condition) session.put(sessionKey, condition)
issues.html.list( issues.html.list(
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit), searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
getMilestones(owner, repoName).filter(_.closedDate.isEmpty), countIssue(condition.copy(state = "open"), filterUser, owner -> repoName),
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName), countIssue(condition.copy(state = "closed"), filterUser, owner -> repoName),
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName), countIssue(condition, Map.empty, owner -> repoName),
countIssue(owner, repoName, condition, "all", None), context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), owner -> repoName)),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))), context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), owner -> repoName)),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))), countIssueGroupByLabels(owner, repoName, condition, filterUser),
countIssueGroupByLabels(owner, repoName, condition, filter, userName),
condition, condition,
filter, filter,
repository, repository,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 => post("/signin", form){ form =>
val account = getAccountByUserName(form.userName) getAccountByUserName(form.userName).collect {
if(account.isEmpty || account.get.password != encrypt(form.password)){ case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
redirect("/signin") session.setAttribute("LOGIN_ACCOUNT", account)
} else { updateLastLoginDate(account.userName)
session.setAttribute("LOGIN_ACCOUNT", account.get)
updateLastLoginDate(account.get.userName)
session.get("REDIRECT").map { redirectUrl => session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT") session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String]) redirect(redirectUrl.asInstanceOf[String])
}.getOrElse { }.getOrElse {
redirect("/%s".format(account.get.userName)) redirect("/")
}
} }
} } getOrElse redirect("/signin")
} }
get("/signout"){ get("/signout"){

View File

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

View File

@@ -5,63 +5,135 @@ import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import jp.sf.amateras.scalatra.forms._ 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 NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean, url: Option[String]) url: Option[String], fileId: Option[String])
val newForm = mapping( case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, unique))), 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)))), "password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100)))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())), "isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))) "url" -> trim(label("URL" , optional(text(maxlength(200))))),
)(UserNewForm.apply) "fileId" -> trim(label("File ID" , optional(text())))
)(NewUserForm.apply)
val editForm = mapping( val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))), "userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))), "password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100)))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" , boolean())), "isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))) "url" -> trim(label("URL" , optional(text(maxlength(200))))),
)(UserEditForm.apply) "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 { 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 { get("/admin/users/_newuser")(adminOnly {
admin.users.html.edit(None) admin.users.html.user(None)
}) })
post("/admin/users/_new", newForm)(adminOnly { form => post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, encrypt(form.password), form.mailAddress, form.isAdmin, form.url) createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
get("/admin/users/:userName/_edit")(adminOnly { get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName") 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") val userName = params("userName")
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy( updateAccount(getAccountByUserName(userName).get.copy(
password = form.password.map(encrypt).getOrElse(account.password), password = form.password.map(sha1).getOrElse(account.password),
mailAddress = form.mailAddress, mailAddress = form.mailAddress,
isAdmin = form.isAdmin, isAdmin = form.isAdmin,
url = form.url)) url = form.url))
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
} getOrElse NotFound } getOrElse NotFound
}) })
private def unique: Constraint = new Constraint(){ get("/admin/users/_newgroup")(adminOnly {
def validate(name: String, value: String): Option[String] = admin.users.html.group(None, Nil)
getAccountByUserName(value).map { _ => "User already exists." } })
}
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
})
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 package service
import model._ import model._
import Accounts._
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
@@ -9,7 +8,10 @@ trait AccountService {
def getAccountByUserName(userName: String): Option[Account] = def getAccountByUserName(userName: String): Option[Account] =
Query(Accounts) filter(_.userName is userName.bind) firstOption Query(Accounts) filter(_.userName is userName.bind) firstOption
def getAccountByMailAddress(mailAddress: String): Option[Account] =
Query(Accounts) filter(_.mailAddress is mailAddress.bind) firstOption
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit = def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
@@ -21,7 +23,9 @@ trait AccountService {
url = url, url = url,
registeredDate = currentDate, registeredDate = currentDate,
updatedDate = currentDate, updatedDate = currentDate,
lastLoginDate = None) lastLoginDate = None,
image = None,
isGroupAccount = false)
def updateAccount(account: Account): Unit = def updateAccount(account: Account): Unit =
Accounts Accounts
@@ -35,8 +39,48 @@ trait AccountService {
account.registeredDate, account.registeredDate,
currentDate, currentDate,
account.lastLoginDate) account.lastLoginDate)
def updateAvatarImage(userName: String, image: Option[String]): Unit =
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
def updateLastLoginDate(userName: String): Unit = def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate) Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
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 Q.interpolation
import model._ import model._
import Issues._
import util.Implicits._ import util.Implicits._
import util.StringUtil._
trait IssuesService { trait IssuesService {
import IssuesService._ import IssuesService._
@@ -36,21 +36,22 @@ trait IssuesService {
.map ( _._2 ) .map ( _._2 )
.list .list
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
/** /**
* Returns the count of the search result against issues. * Returns the count of the search result against issues.
* *
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by") * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param userName the filter user name required for "assigned" and "created_by" * @param repos Tuple of the repository owner and the repository name
* @return the count of the search result * @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. // 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). // But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170 // 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. * Returns the Map which contains issue count for each labels.
@@ -58,14 +59,13 @@ trait IssuesService {
* @param owner the repository owner * @param owner the repository owner
* @param repository the repository name * @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by") * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @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)
* @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, 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) => .innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
} }
@@ -80,73 +80,93 @@ trait IssuesService {
} }
.toMap .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. * Returns the search result against issues.
* *
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by") * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param userName the filter user name required for "assigned" and "created_by"
* @param offset the offset for pagination * @param offset the offset for pagination
* @param limit the limit 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) * @return the search result (list of tuples which contain issue, labels and comment count)
*/ */
def searchIssue(owner: String, repository: String, condition: IssueSearchCondition, def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String],
filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = { offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
// get issues and comment count // get issues and comment count and labels
val issues = searchIssueQuery(owner, repository, condition, filter, userName) searchIssueQuery(repos, condition, filterUser)
.leftJoin(Query(IssueComments) .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.filter { _.byRepository(owner, repository) } .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.groupBy { _.issueId } .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1) .map { case (((t1, t2), t3), t4) =>
.sortBy { case (t1, t2) => (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
(condition.sort match { }
case "created" => t1.registeredDate .sortBy(_._4) // labelName
case "comments" => t2._2 .sortBy { case (t1, commentCount, _,_,_) =>
case "updated" => t1.updatedDate (condition.sort match {
}) match { case "created" => t1.registeredDate
case sort => condition.direction match { case "comments" => commentCount
case "asc" => sort asc case "updated" => t1.updatedDate
case "desc" => sort desc }) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
}
} }
} }
} .drop(offset).take(limit)
.map { case (t1, t2) => (t1, t2._2.ifNull(0)) } .list
.drop(offset).take(limit) .splitWith { (c1, c2) =>
.list c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
// get labels c1._1.issueId == c2._1.issueId
val labels = Query(IssueLabels) }
.innerJoin(Labels).on { (t1, t2) => .map { issues => issues.head match {
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) case (issue, commentCount, _,_,_) =>
} (issue,
.filter { case (t1, t2) => issues.flatMap { t => t._3.map (
(t1.byRepository(owner, repository)) && Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
(t1.issueId inSetBind (issues.map(_._1.issueId))) )} toList,
} commentCount)
.sortBy { case (t1, t2) => t1.issueId ~ t2.labelName } }} toList
.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)
}
} }
/** /**
* Assembles query for conditional issue searching. * 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 => 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.closed is (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) && (t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.assignedUserName is userName.get.bind, filter == "assigned") && (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName is userName.get.bind, filter == "created_by") && (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(IssueLabels filter { t2 => (IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in (t2.labelId in
@@ -189,7 +209,7 @@ trait IssuesService {
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def createComment(owner: String, repository: String, loginUser: String, def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: Option[String]) = issueId: Int, content: String, action: String) =
IssueComments.autoInc insert ( IssueComments.autoInc insert (
owner, owner,
repository, repository,
@@ -231,10 +251,63 @@ trait IssuesService {
} }
.update (closed, currentDate) .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 { object IssuesService {
import java.net.URLEncoder
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
val IssueLimit = 30 val IssueLimit = 30
@@ -242,12 +315,11 @@ object IssuesService {
case class IssueSearchCondition( case class IssueSearchCondition(
labels: Set[String] = Set.empty, labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None, milestoneId: Option[Option[Int]] = None,
repo: Option[String] = None,
state: String = "open", state: String = "open",
sort: String = "created", sort: String = "created",
direction: String = "desc"){ direction: String = "desc"){
import IssueSearchCondition._
def toURL: String = def toURL: String =
"?" + List( "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
@@ -255,6 +327,7 @@ object IssuesService {
case Some(x) => x.toString case Some(x) => x.toString
case None => "none" case None => "none"
})}, })},
repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)), Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)), Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&") Some("direction=" + urlEncode(direction))).flatten.mkString("&")
@@ -263,8 +336,6 @@ object IssuesService {
object IssueSearchCondition { object IssueSearchCondition {
private def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
val value = request.getParameter(name) val value = request.getParameter(name)
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
@@ -277,8 +348,17 @@ object IssuesService {
case "none" => None case "none" => None
case x => Some(x.toInt) case x => Some(x.toInt)
}), }),
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) 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 Database.threadLocalSession
import model._ import model._
import Milestones._
trait MilestonesService { 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 package service
import model._ import model._
import Repositories._
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
import util.JGitUtil import util.JGitUtil
@@ -12,19 +11,17 @@ trait RepositoryService { self: AccountService =>
/** /**
* Creates a new repository. * Creates a new repository.
* *
* The project is created as public repository at first. Users can modify the project type at the repository settings
* page after the project creation to configure the project as the private repository.
*
* @param repositoryName the repository name * @param repositoryName the repository name
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param description the repository description * @param description the repository description
* @param isPrivate the repository type (private is true, otherwise false)
*/ */
def createRepository(repositoryName: String, userName: String, description: Option[String]): Unit = { def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean): Unit = {
Repositories insert Repositories insert
Repository( Repository(
userName = userName, userName = userName,
repositoryName = repositoryName, repositoryName = repositoryName,
isPrivate = false, isPrivate = isPrivate,
description = description, description = description,
defaultBranch = "master", defaultBranch = "master",
registeredDate = currentDate, registeredDate = currentDate,
@@ -35,9 +32,15 @@ trait RepositoryService { self: AccountService =>
} }
def deleteRepository(userName: String, repositoryName: String): Unit = { def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete Collaborators .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete Issues .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete Repositories .filter(_.byRepository(userName, repositoryName)).delete
} }
@@ -50,40 +53,6 @@ trait RepositoryService { self: AccountService =>
def getRepositoryNamesOfUser(userName: String): List[String] = def getRepositoryNamesOfUser(userName: String): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list 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. * Returns the specified repository information.
* *
@@ -94,36 +63,48 @@ trait RepositoryService { self: AccountService =>
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags)
} }
} }
/** /**
* Returns the list of accessible repositories information for the specified account user. * Returns the list of specified user's repositories.
* * It contains own repositories and collaboration repositories.
* @param account the account
* @param baseUrl the base url of this application
* @return the repository informations which is sorted in descending order of lastActivityDate.
*/ */
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = { def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
Query(Repositories).filter { t1 =>
def createRepositoryInfo(repository: Repository): RepositoryInfo = { (t1.userName is userName.bind) ||
val repositoryInfo = JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) (Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
RepositoryInfo(repositoryInfo.owner, repositoryInfo.name, repositoryInfo.url, repository, repositoryInfo.branchList, repositoryInfo.tags) }.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 // for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories) case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) || 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 // for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind) case None => Query(Repositories) filter(_.isPrivate is false.bind)
}).sortBy(_.lastActivityDate desc).list.map(createRepositoryInfo _) }).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
} }
/** /**
@@ -161,6 +142,15 @@ trait RepositoryService { self: AccountService =>
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete 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. * Returns the list of collaborators name which is sorted with ascending order.
* *
@@ -185,6 +175,12 @@ trait RepositoryService { self: AccountService =>
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
branchList: List[String], tags: List[util.JGitUtil.TagInfo]) commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
def this(repo: JGitUtil.RepositoryInfo, model: Repository) = {
this(repo.owner, repo.name, repo.url, model, repo.commitCount, repo.branchList, repo.tags)
}
}
} }

View File

@@ -0,0 +1,26 @@
package service
import model._
/**
* This service is used for a view helper mainly.
*
* It may be called many times in one request, so each method stores
* its result into the cache which available during a request.
*/
trait RequestCache {
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
new IssuesService {}.getIssue(userName, repositoryName, issueId)
}
}
def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${userName}"){
new AccountService {}.getAccountByUserName(userName)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.util.io.DisabledOutputStream import org.eclipse.jgit.util.io.DisabledOutputStream
import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
@@ -26,10 +27,11 @@ object JGitUtil {
* @param owner the user name of the repository owner * @param owner the user name of the repository owner
* @param name the repository name * @param name the repository name
* @param url the repository URL * @param url the repository URL
* @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001.
* @param branchList the list of branch names * @param branchList the list of branch names
* @param tags the list of tags * @param tags the list of tags
*/ */
case class RepositoryInfo(owner: String, name: String, url: String, branchList: List[String], tags: List[TagInfo]) case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo])
/** /**
* The file data for the file list of the repository viewer. * The file data for the file list of the repository viewer.
@@ -50,14 +52,21 @@ object JGitUtil {
* @param id the commit id * @param id the commit id
* @param time the commit time * @param time the commit time
* @param committer the committer name * @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message * @param shortMessage the short message
* @param fullMessage the full message * @param fullMessage the full message
* @param parents the list of parent commit id * @param parents the list of parent commit id
*/ */
case class CommitInfo(id: String, time: Date, committer: String, shortMessage: String, fullMessage: String, parents: List[String]){ case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
shortMessage: String, fullMessage: String, parents: List[String]){
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
rev.getName, rev.getCommitterIdent.getWhen, rev.getCommitterIdent.getName, rev.getShortMessage, rev.getFullMessage, rev.getName,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress,
rev.getShortMessage,
rev.getFullMessage,
rev.getParents().map(_.name).toList) rev.getParents().map(_.name).toList)
val summary = { val summary = {
@@ -141,18 +150,35 @@ object JGitUtil {
*/ */
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
withGit(getRepositoryDir(owner, repository)){ git => withGit(getRepositoryDir(owner, repository)){ git =>
RepositoryInfo( try {
owner, repository, baseUrl + "/git/%s/%s.git".format(owner, repository), // get commit count
// branches val i = git.log.all.call.iterator
git.branchList.call.asScala.map { ref => var commitCount = 0
ref.getName.replaceFirst("^refs/heads/", "") while(i.hasNext && commitCount <= 1000){
}.toList, i.next
// tags commitCount = commitCount + 1
git.tagList.call.asScala.map { ref => }
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName) RepositoryInfo(
}.toList owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
) // commit count
commitCount,
// branches
git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "")
}.toList,
// tags
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList
)
} catch {
// not initialized
case e: NoHeadException => RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil)
}
} }
} }
@@ -234,7 +260,7 @@ object JGitUtil {
* @param path filters by this path. default is no filter. * @param path filters by this path. default is no filter.
* @return a tuple of the commit list and whether has next * @return a tuple of the commit list and whether has next
*/ */
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): (List[CommitInfo], Boolean) = { def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = {
val fixedPage = if(page <= 0) 1 else page val fixedPage = if(page <= 0) 1 else page
@scala.annotation.tailrec @scala.annotation.tailrec
@@ -248,20 +274,25 @@ object JGitUtil {
} }
val revWalk = new RevWalk(git.getRepository) val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision))) val objectId = git.getRepository.resolve(revision)
if(path.nonEmpty){ if(objectId == null){
revWalk.setRevFilter(new RevFilter(){ Left(s"${revision} can't be resolved.")
def include(walk: RevWalk, commit: RevCommit): Boolean = { } else {
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty revWalk.markStart(revWalk.parseCommit(objectId))
} if(path.nonEmpty){
override def clone(): RevFilter = this revWalk.setRevFilter(new RevFilter(){
}) def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
}
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
Right(commits)
} }
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
commits
} }
/** /**
@@ -317,51 +348,11 @@ object JGitUtil {
* @return the list of latest commit * @return the list of latest commit
*/ */
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = { def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val start = git.getRepository.resolve(revision)
val map = new scala.collection.mutable.HashMap[String, RevCommit] paths.map { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
val revWalk = new RevWalk(git.getRepository) (path, commit)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision))) }.toMap
//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
} }
/** /**
@@ -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 package util
import java.net.{URLDecoder, URLEncoder}
object StringUtil { object StringUtil {
def encrypt(value: String): String = { def sha1(value: String): String = {
val md = java.security.MessageDigest.getInstance("SHA-1") val md = java.security.MessageDigest.getInstance("SHA-1")
md.update(value.getBytes) md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString md.digest.map(b => "%02x".format(b)).mkString
} }
def md5(value: String): String = {
val md = java.security.MessageDigest.getInstance("MD5")
md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString
}
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
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 package util
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import scala.Some
trait Validations { trait Validations {
@@ -11,9 +10,9 @@ trait Validations {
def identifier: Constraint = new Constraint(){ def identifier: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = def validate(name: String, value: String): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){ if(!value.matches("^[a-zA-Z0-9\\-_]+$")){
Some("%s contains invalid character.".format(name)) Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
Some("%s starts with invalid character.".format(name)) Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }

View File

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

View File

@@ -2,11 +2,13 @@ package view
import java.util.Date import java.util.Date
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import twirl.api.Html import twirl.api.Html
import util.StringUtil
import service.RequestCache
/** /**
* Provides helper methods for Twirl templates. * Provides helper methods for Twirl templates.
*/ */
object helpers { object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
/** /**
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss". * Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
@@ -29,39 +31,63 @@ object helpers {
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): Html = { enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
} }
/**
* Returns &lt;img&gt; which displays the avatar icon.
* Looks up Gravatar if avatar icon has not been configured in user settings.
*/
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
getAvatarImageHtml(userName, size, "", tooltip)
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
/**
* Converts commit id, issue id and username to the link.
*/
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
Html(convertRefsLinks(value, repository))
def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1">$$1</a>""")
)
def urlEncode(value: String): String = StringUtil.urlEncode(value)
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
/** /**
* Generates the url to the repository. * Generates the url to the repository.
*/ */
def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String = def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String =
"%s/%s/%s".format(context.path, repository.owner, repository.name) s"${context.path}/${repository.owner}/${repository.name}"
/** /**
* Generates the url to the account page. * Generates the url to the account page.
*/ */
def url(userName: String)(implicit context: app.Context): String = "%s/%s".format(context.path, userName) def url(userName: String)(implicit context: app.Context): String =
s"${context.path}/${userName}"
/** /**
* Returns the url to the root of assets. * Returns the url to the root of assets.
*/ */
def assets(implicit context: app.Context): String = "%s/assets".format(context.path) def assets(implicit context: app.Context): String =
s"${context.path}/assets"
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 = implicit class RichHtmlSeq(seq: Seq[Html]) {
Html(value
// escape HTML tags
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
// convert issue id to link
.replaceAll("(^|\\W)#(\\d+)(\\W|$)", "$1<a href=\"%s/%s/%s/issues/$2\">#$2</a>$3".format(context.path, repository.owner, repository.name))
// convert commit id to link
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", "$1<a href=\"%s/%s/%s/commit/$2\">$2</a>$3").format(context.path, repository.owner, repository.name))
implicit def extendsHtmlSeq(seq: Seq[Html]) = new {
def mkHtml(separator: String) = Html(seq.mkString(separator)) def mkHtml(separator: String) = Html(seq.mkString(separator))
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))
} }

View File

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

View File

@@ -1,49 +0,0 @@
@(account: model.Account, repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
<ul class="nav nav-tabs">
<li class="active"><a href="#">Repositories</a></li>
<!--
<li><a href="#">Activity</a></li>
-->
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
}
</div>
</div>
</div>
}

View File

@@ -0,0 +1,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"}> <li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a> <a href="@path/admin/system">System Settings</a>
</li> </li>
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul> </ul>
</div> </div>
</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 context._
@import view.helpers._ @import view.helpers._
@html.main("System Settings"){ @html.main("System Settings"){
@menu("system"){ @menu("system"){
@helper.html.information(info)
<form action="@path/admin/system" method="POST" validate="true"> <form action="@path/admin/system" method="POST" validate="true">
<div class="box"> <div class="box">
<div class="box-header">System Settings</div> <div class="box-header">System Settings</div>

View File

@@ -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 context._
@import view.helpers._ @import view.helpers._
@html.main("Manage Users"){ @html.main("Manage Users"){
@admin.html.menu("users"){ @admin.html.menu("users"){
<div style="text-align: right; margin-bottom: 4px;"> <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> </div>
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
@users.map { account => @users.map { account =>
<tr> <tr>
<td> <td>
<div class="pull-right"> <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>
<div class="strong"> <div class="strong">
@avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a> <a href="@url(account.userName)">@account.userName</a>
@if(account.isAdmin){ @if(account.isGroupAccount){
(Administrator) (Group)
} else { } else {
(Normal) @if(account.isAdmin){
(Administrator)
} else {
(Normal)
}
}
@if(account.isGroupAccount){
@members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true)
}
} }
</div> </div>
<div> <div>
<hr> <hr>
<i class="icon-envelope"></i> @account.mailAddress @if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress
}
@account.url.map { url => @account.url.map { url =>
<i class="icon-home"></i> @url <i class="icon-home"></i> @url
} }
@@ -31,7 +48,9 @@
<div> <div>
<span class="muted">Registered:</span> @datetime(account.registeredDate) <span class="muted">Registered:</span> @datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate) <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> </div>
</td> </td>
</tr> </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"> <div class="btn-group">
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown"> <button class="btn btn-mini dropdown-toggle" data-toggle="dropdown">
<i class="icon-cog"></i> @if(buttonValue == ""){
<i class="icon-cog"></i>
} else {
<strong>@buttonValue</strong>
}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">

View File

@@ -0,0 +1,7 @@
@(info: Option[Any])
@if(info.isDefined){
<div class="alert alert-info">
<button type="button" class="close" data-dismiss="alert">×</button>
@info
</div>
}

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
@(account: Option[model.Account])(implicit context: app.Context)
@import context._
<div id="avatar" class="muted">
@if(account.nonEmpty && account.get.image.nonEmpty){
<img src="@path/@account.get.userName/_avatar" style="with: 120px; height: 120px;"/>
} else {
<div id="clickable">
<a href="https://www.gravatar.com/" target="_blank">Gravatar</a> is used
</div>
}
</div>
@if(account.nonEmpty && account.get.image.nonEmpty){
<label>
<input type="checkbox" name="clearImage"/> Clear image
</label>
}
<input type="hidden" name="fileId" value=""/>
<script>
$(function(){
var dropzone = new Dropzone('div#clickable', {
url: '@path/upload/image',
previewsContainer: 'div#avatar',
paramName: 'file',
parallelUploads: 1,
thumbnailWidth: 120,
thumbnailHeight: 120
});
dropzone.on("success", function(file, id){
$('div#clickable').remove();
$('input[name=fileId]').val(id);
});
});
</script>
<style type="text/css">
div.dz-filename, div.dz-size, div.dz-progress, div.dz-success-mark, div.dz-error-mark, div.dz-error-message {
display: none;
}
div#clickable {
width: 100%;
text-align: center;
line-height: 120px;
}
div#avatar {
background-color: #f8f8f8;
border: 1px dashed silver;
width: 120px;
height: 120px;
}
</style>

View File

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

View File

@@ -5,25 +5,25 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("New Issue - " + repository.owner + "/" + repository.name){ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository) @html.header("issues", repository)
@tab("", repository) @tab("", repository)
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span9"> <div class="span9">
<div class="box"> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box">
<div class="box-content"> <div class="box-content">
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 650px;"/> <input type="text" name="title" value="" placeholder="Title" style="width: 600px;"/>
<div> <div>
<span id="label-assigned">No one is assigned</span> <span id="label-assigned">No one is assigned</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="assignedUserName" value=""/> <input type="hidden" name="assignedUserName" value=""/>
@helper.html.dropdown { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li> <li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i> @collaborator</a></li> <li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
} }
} }
} }
@@ -31,21 +31,37 @@
<span id="label-milestone">No milestone</span> <span id="label-milestone">No milestone</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/> <input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li> <li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone => @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>
</div> </div>
<hr> <hr>
@helper.html.preview(repository, "", false, true, true, "width: 650px; height: 200px;") @helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
</div> </div>
</div> </div>
<input type="submit" class="btn btn-success" value="Submit new issue"/> <div class="pull-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/>
</div>
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
@@ -88,7 +104,7 @@ $(function(){
}); });
$('a.milestone').click(function(){ $('a.milestone').click(function(){
var title = $(this).text(); var title = $(this).data('title');
var milestoneId = $(this).data('id'); var milestoneId = $(this).data('id');
$('a.milestone i.icon-ok').attr('class', 'icon-white'); $('a.milestone i.icon-ok').attr('class', 'icon-white');
@@ -105,13 +121,15 @@ $(function(){
if($(this).data('selected') == true){ if($(this).data('selected') == true){
$(this).css({ $(this).css({
'background-color': 'white', 'background-color': 'white',
'color' : 'black' 'color' : 'black',
'font-weight' : 'normal'
}); });
$(this).data('selected', false); $(this).data('selected', false);
} else { } else {
$(this).css({ $(this).css({
'background-color': '#' + $(this).data('bgcolor'), 'background-color': '#' + $(this).data('bgcolor'),
'color' : '#' + $(this).data('fgcolor') 'color' : '#' + $(this).data('fgcolor'),
'font-weight' : 'bold'
}); });
$(this).data('selected', true); $(this).data('selected', true);
} }

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(issues: List[(model.Issue, List[model.Label], Int)],
page: Int, page: Int,
labels: List[model.Label], collaborators: List[String],
milestones: List[model.Milestone], milestones: List[model.Milestone],
labels: List[model.Label],
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
allCount: Int, allCount: Int,
@@ -14,7 +15,7 @@
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Issues - " + repository.owner + "/" + repository.name){ @html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository) @html.header("issues", repository)
@tab("issues", repository) @tab("issues", repository)
<div class="row-fluid"> <div class="row-fluid">
@@ -51,7 +52,7 @@
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title) <span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
} }
} }
@helper.html.dropdown { @helper.html.dropdown() {
@if(condition.milestoneId.isDefined){ @if(condition.milestoneId.isDefined){
<li> <li>
<a href="@condition.copy(milestoneId = None).toURL"> <a href="@condition.copy(milestoneId = None).toURL">
@@ -64,21 +65,45 @@
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone @helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
</a> </a>
</li> </li>
@milestones.map { milestone => @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li> <li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL"> <a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title @helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
</a> <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> </li>
} }
} }
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){ @if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
<div class="milestone-progress" style="margin-top: 8px;"> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
@if(closedCount > 0){ <div style="margin-top: 4px;">
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span> @_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/> <hr/>
<strong>Labels</strong> <strong>Labels</strong>
@@ -106,99 +131,15 @@
@_root_.issues.labels.html.edit(None, repository) @_root_.issues.labels.html.edit(None, repository)
} }
</div> </div>
<div class="span9"> @***** show issue list *****@
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ @listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
<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;
<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>
</div> </div>
@if(hasWritePermission){
<form id="batcheditForm" method="POST">
<input type="hidden" name="value"/>
<input type="hidden" name="checked"/>
</form>
}
} }
@if(hasWritePermission){ @if(hasWritePermission){
<script> <script>
@@ -213,6 +154,33 @@ $(function(){
}); });
} }
}); });
$('.table-issues input[type=checkbox]').change(function(){
$('.table-issues button').prop('disabled',
!$('.table-issues input[type=checkbox]').filter(':checked').length);
}).filter(':first').change();
var submitBatchEdit = function(action, value) {
var checked = $('.table-issues input[type=checkbox]').filter(':checked').map(function(){ return this.value; }).get().join();
var form = $('#batcheditForm');
form.find('input[name=value]').val(value);
form.find('input[name=checked]').val(checked);
form.attr('action', action);
form.submit();
};
$('#state').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase());
});
$('a.toggle-label').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id'));
});
$('a.toggle-assign').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/assign', $(this).data('name'));
});
$('a.toggle-milestone').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/milestone', $(this).data('id'));
});
}); });
</script> </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) @(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository) @html.header("milestones", repository)
@issues.html.tab("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"> <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) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository) @html.header("milestones", repository)
@issues.html.tab("milestones", repository) @issues.html.tab("milestones", repository)
<div class="row-fluid"> <div class="row-fluid">
@@ -42,9 +42,13 @@
@if(milestone.closedDate.isDefined){ @if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span> <span class="muted">Closed @datetime(milestone.closedDate.get)</span>
} else { } else {
@if(milestone.dueDate.isDefined){ @milestone.dueDate.map { dueDate =>
<span class="muted">Due in @date(milestone.dueDate.get)</span> @if(isPast(dueDate)){
} else { <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> <span class="muted">No due date</span>
} }
} }
@@ -65,23 +69,12 @@
</div> </div>
<span class="muted">@closedCount closed - @openCount open</span> <span class="muted">@closedCount closed - @openCount open</span>
</div> </div>
<div class="milestone-progress"> @progress(openCount + closedCount, closedCount, true)
@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>
</div> </div>
</div> </div>
@if(milestone.description.isDefined){ @if(milestone.description.isDefined){
<div class="milestone-description"> <div class="milestone-description">
@markdown(milestone.description.get, repository, false, false, false) @markdown(milestone.description.get, repository, false, false)
</div> </div>
} }
</td> </td>

View File

@@ -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 context._
@import view.helpers._ @import view.helpers._
<!DOCTYPE html> <!DOCTYPE html>
@@ -19,45 +19,60 @@
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/> <link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/common/css/gitbucket.css" rel="stylesheet"> <link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/common/js/jquery-1.9.1.js"></script> <script src="@assets/common/js/jquery-1.9.1.js"></script>
<script src="@assets/common/js/dropzone.js"></script>
<script src="@assets/common/js/validation.js"></script> <script src="@assets/common/js/validation.js"></script>
<script src="@assets/common/js/gitbucket.js"></script> <script src="@assets/common/js/gitbucket.js"></script>
<script src="@assets/bootstrap/js/bootstrap.js"></script> <script src="@assets/bootstrap/js/bootstrap.js"></script>
<script src="@assets/datepicker/js/bootstrap-datepicker.js"></script> <script src="@assets/datepicker/js/bootstrap-datepicker.js"></script>
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script> <script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/google-code-prettify/prettify.js"></script> <script src="@assets/google-code-prettify/prettify.js"></script>
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
</head> </head>
<body> <body>
<div class="navbar navbar-inverse"> <form id="search" action="@path/search" method="POST">
<div class="navbar-inner"> <div class="navbar">
<div class="container"> <div class="navbar-inner">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <div class="container">
<span class="icon-bar"></span> <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> <span class="icon-bar"></span>
</button> <span class="icon-bar"></span>
<a class="brand" href="@path/">GitBucket</a> </button>
<div class="nav-collapse collapse"> <a class="brand" href="@path/">GitBucket</a>
<ul class="nav pull-right"> <div class="nav-collapse collapse pull-right header-menu">
@if(loginAccount.isDefined){ @repository.map { repository =>
<li><a href="@path/new">New repo</a></li> <input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
<li><a href="@url(loginAccount.get.userName)">Account</a></li> <input type="hidden" name="owner" value="@repository.owner"/>
@if(loginAccount.get.isAdmin){ <input type="hidden" name="repository" value="@repository.name"/>
<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>
} }
</ul> @if(loginAccount.isDefined){
</div><!--/.nav-collapse --> <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>
</div> </form>
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
}
</div> </div>
<div class="container body"> <div class="container body">
@body @body
</div> </div>
<script>
$(function(){
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
});
</script>
</body> </body>
<script> </html>
$(function(){ prettyPrint(); });
</script>
</html>

View File

@@ -1,18 +1,73 @@
@()(implicit context: app.Context) @(groupNames: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._
@main("Create a New Repository"){ @main("Create a New Repository"){
<form id="form" method="post" action="@path/new" validate="true"> <div style="width: 600px; margin: 10px auto;">
<fieldset> <form id="form" method="post" action="@path/new" validate="true">
<label for="name"><strong>Repository name</strong></label> <fieldset>
<input type="text" name="name" id="name" /> <label for="name"><strong>Repository name</strong></label>
<span id="error-name" class="error"></span> <div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
</fieldset> <button class="btn dropdown-toggle" data-toggle="dropdown">
<fieldset> <strong>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</strong>
<label for="description"><strong>Description</strong> (optional)</label> <span class="caret"></span>
<input type="text" name="description" id="description" style="width: 600px;"/> </button>
</fieldset> <ul class="dropdown-menu">
<fieldset> <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>
<input type="submit" class="btn btn-success" value="Create repository"/> @groupNames.map { groupName =>
</fieldset> <li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
</form> }
} </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) latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(repository.owner+"/"+repository.name) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository) @html.header("code", repository)
@tab(branch, repository, "files") @tab(branch, repository, "files")
<div class="head"> <div class="head">
@@ -23,6 +23,7 @@
<tr> <tr>
<th style="font-weight: normal;"> <th style="font-weight: normal;">
<div class="pull-left"> <div class="pull-left">
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a> <a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
<span class="muted">@datetime(latestCommit.time)</span> <span class="muted">@datetime(latestCommit.time)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a> <a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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