Compare commits

..

229 Commits
1.12 ... 2.1

Author SHA1 Message Date
Naoki Takezoe
104c3bc89d Disable TestCase for Services 2014-07-06 17:35:18 +09:00
Naoki Takezoe
2668977918 Convert CRLF to LF 2014-07-06 17:07:52 +09:00
Naoki Takezoe
28424c96c4 Readying 2.1 release 2014-07-06 17:06:26 +09:00
Naoki Takezoe
9cfa8c594b Merge branch 'master' into slick2
Conflicts:
	project/build.scala
	src/main/scala/app/IndexController.scala
	src/main/scala/app/RepositorySettingsController.scala
	src/main/scala/model/Account.scala
	src/main/scala/model/BasicTemplate.scala
	src/main/scala/model/Issue.scala
	src/main/scala/model/IssueComment.scala
	src/main/scala/model/package.scala
	src/main/scala/service/IssuesService.scala
	src/main/scala/service/PullRequestService.scala
	src/main/scala/service/RepositoryService.scala
	src/main/scala/service/WikiService.scala
	src/main/scala/servlet/TransactionFilter.scala
	src/main/scala/util/Notifier.scala
2014-07-06 17:02:49 +09:00
Naoki Takezoe
5c70cd654c (refs #341)Add TODO about Slick 2.0 migration 2014-07-06 16:21:25 +09:00
Naoki Takezoe
7aca24e51d (refs #341)Fix compilation error about date conversion 2014-07-06 16:09:50 +09:00
Naoki Takezoe
cce0b67871 (refs #341)Fix compilation error of delete statements 2014-07-06 15:42:45 +09:00
Naoki Takezoe
606cd83f44 Fix CRLF to LF 2014-07-06 13:19:27 +09:00
Naoki Takezoe
32897c36f9 (refs #341)Fix compilation error other than date mapping and delete statement 2014-07-06 13:07:05 +09:00
Naoki Takezoe
92e4e12655 Update README.md 2014-07-05 18:06:51 +09:00
Naoki Takezoe
c8e5b75165 Disable front-end of plugin system in GitBucket 2.1 2014-07-05 18:01:19 +09:00
Naoki Takezoe
09b9a52ad3 Merge branch 'plugin-system' 2014-07-05 17:36:02 +09:00
Naoki Takezoe
33378c6464 Merge pull request #425 from okapies/fix-ldap-filter
Filter by username explicitly
2014-07-05 11:54:08 +09:00
shimamoto
259bcfc14f (refs #341) Fix compile errors. 2014-07-01 04:08:15 +09:00
shimamoto
c361d24ba4 (refs #341) Implement a method to get the session. 2014-07-01 03:43:55 +09:00
shimamoto
d5e1b18b52 (refs #341) Sets default value. 2014-07-01 03:40:21 +09:00
tanacasino
684a17a15b Merge pull request #422 from tanacasino/improve-markdown-css
Make markdown looks like more GitHub
2014-07-01 02:44:11 +09:00
Yuta Okamoto
66b7b69d20 specify LDAP search filter explicitly 2014-06-30 23:38:43 +09:00
Tomofumi Tanaka
57254f6366 Make markdown looks like more GitHub 2014-06-29 22:17:12 +09:00
Naoki Takezoe
c64909ab1a Plugin updating is executed asynchronously by Quartz Scheduler 2014-06-29 16:09:41 +09:00
Naoki Takezoe
34dd8541f4 Disable JavaScript console 2014-06-29 14:35:49 +09:00
Naoki Takezoe
50b4fb154d Fix JavaScript path 2014-06-29 14:33:48 +09:00
Naoki Takezoe
0b3781ec8a Add plugin updating capability 2014-06-29 14:26:33 +09:00
Naoki Takezoe
0e1d184715 Remove installed plugins from available plugin list 2014-06-29 12:39:27 +09:00
Naoki Takezoe
d8c27046f6 Merge branch 'master' into plugin-system
Conflicts:
	src/main/scala/servlet/AutoUpdateListener.scala
2014-06-29 04:20:58 +09:00
Naoki Takezoe
fd09058a7d (refs #310)Convert all CRLF to LF 2014-06-29 04:16:37 +09:00
Naoki Takezoe
1c99b57709 (refs #412)Fix repository lock 2014-06-29 03:29:03 +09:00
shimamoto
9ee739d102 (refs #341) Migrate slick session. 2014-06-27 08:48:58 +09:00
Tomofumi Tanaka
e2cde81b72 (refs #417) Correct wiki repository url 2014-06-25 23:40:41 +09:00
Tomofumi Tanaka
84a4b8fd92 (refs #415) Fix bug 2014-06-25 00:19:28 +09:00
shimamoto
d2c94909cb (refs #341) Migrate service package. 2014-06-24 02:40:40 +09:00
Naoki Takezoe
a9bfe0dfab (refs #411)Move thirdparty JavaScript and CSS to vendors/ 2014-06-20 11:51:27 +09:00
Naoki Takezoe
9af81c7093 Merge pull request #410 from cranst0n/ant-deprecation
Remove deprecated Ant 'rename' task.
2014-06-20 09:25:43 +09:00
cranst0n
1e8a5c3cde Remove deprecated 'rename' task. 2014-06-19 13:21:24 -04:00
Naoki Takezoe
707ad866e1 (refs #408)Performance improvement for index page. 2014-06-20 01:42:42 +09:00
Naoki Takezoe
c3a944b40e (refs #405)Fix styles 2014-06-16 10:59:21 +09:00
Naoki Takezoe
ab80cb8f60 (refs #405)Fix styles 2014-06-16 10:39:27 +09:00
Naoki Takezoe
4f45e047d2 (refs #406)Fix pull request count in the dashboard 2014-06-16 01:40:51 +09:00
shimamoto
bbe455ac49 (refs #341) Migrate model package. 2014-06-16 01:29:56 +09:00
Naoki Takezoe
b5f173fa46 (refs #32)Display plugin's status 2014-06-16 01:20:42 +09:00
Naoki Takezoe
4bd6ef143a (refs #32)Clone or pull plugin repository before displaying the available plugins page 2014-06-15 18:07:34 +09:00
Naoki Takezoe
fd4a696303 (refs #32)Add version to plugin meta information 2014-06-15 17:39:42 +09:00
Naoki Takezoe
4af4c4e7c6 (refs #32)Add plugin install tab 2014-06-15 13:11:08 +09:00
Naoki Takezoe
3b2e42fd61 (refs #32)Making plugin administration pages 2014-06-14 23:29:37 +09:00
Naoki Takezoe
b07d0b028f (refs #32)Add deleting installed plugins 2014-06-14 14:32:40 +09:00
Naoki Takezoe
f3900ca8f9 (refs #32)Add plugin system initialization 2014-06-14 14:00:21 +09:00
Naoki Takezoe
62d43f120a (refs #32)Separate Plugin interface and implementation 2014-06-14 13:26:56 +09:00
Naoki Takezoe
c4f69fbd13 (refs #32)Switch JavaScript processor to Rhino from Nashorn because GitBucket should work on both of JDK7 and JDK8 2014-06-14 10:58:58 +09:00
Naoki Takezoe
fece20ff40 (refs #32)Implementing repository action processing 2014-06-11 01:45:51 +09:00
Naoki Takezoe
bbef4b22ca (refs #32)Add plugins page into the system admin tools 2014-06-10 10:44:36 +09:00
Naoki Takezoe
481a2d213f (refs #32)Improving plug-in API 2014-06-10 07:43:52 +09:00
Tomofumi Tanaka
8ed4075f1e Change word to clear milestone the same as GitHub 2014-06-10 00:40:09 +09:00
Tomofumi Tanaka
9bf82733d1 Make icon-remove-circle of dropdown menu unclickable 2014-06-10 00:24:30 +09:00
Tomofumi Tanaka
30d66f95bc Show number of conversations 2014-06-09 22:49:41 +09:00
Naoki Takezoe
378c2c39a8 (refs #32)Add JavaScript API 2014-06-08 00:37:01 +09:00
Naoki Takezoe
daf5fc434c Merge remote-tracking branch 'origin/plugin-system' into plugin-system 2014-06-07 18:53:31 +09:00
Naoki Takezoe
e5bf90ed26 (refs #32)Provide generic layout and context to custom actions 2014-06-07 18:53:07 +09:00
Tomofumi Tanaka
1bf3146220 (refs #396)Apply syntax highlight correctly when update comment 2014-06-06 22:22:51 +09:00
Naoki Takezoe
ddd51850f0 (refs #32)Add JavaScript Console 2014-06-06 17:20:48 +09:00
Naoki Takezoe
e14a0c3770 (refs #32)Enable menu icon which is injected by plug-in 2014-06-06 16:35:54 +09:00
Naoki Takezoe
b0b318ce30 (refs #32)Example of custom action extension 2014-06-05 21:22:16 +09:00
Naoki Takezoe
6f666ca49f Merge branch 'master' into plugin-system 2014-06-05 20:54:28 +09:00
Naoki Takezoe
0cb2116bdf (refs #32)First impression of the plugin system 2014-06-05 20:52:38 +09:00
Naoki Takezoe
280113497b Merge pull request #390 from sakapoko/navform
Fix nested tags.
2014-06-05 09:37:40 +09:00
Shuji Sakagami
5f6e318329 Fix nested tags. 2014-06-04 13:11:37 +09:00
takezoe
f8921b6f10 Add a badge which shows number of pages 2014-06-03 07:28:30 +09:00
Naoki Takezoe
31a08abff2 (refs #378)"Delete branch" button is displayed for only merged pull requests 2014-06-02 21:34:03 +09:00
Naoki Takezoe
0fa1e11c5a (refs #378)Closed but not merged pull requests should be re-openable 2014-06-02 21:28:20 +09:00
Naoki Takezoe
1edff41690 Fix code style 2014-06-02 16:10:03 +09:00
Naoki Takezoe
6d6f529d40 (refs #380)Close stream certainly 2014-06-02 16:04:45 +09:00
Naoki Takezoe
e2fd7d9d8e Fix "New pull request" button style 2014-06-01 23:46:28 +09:00
Naoki Takezoe
61146687b3 Merge remote-tracking branch 'origin/master' 2014-06-01 22:58:24 +09:00
Naoki Takezoe
1d1f7fa581 (refs #382)Remove unnecessary comment 2014-06-01 22:58:07 +09:00
Tomofumi Tanaka
67da88fab5 Use "Conversation" instead of "Discussion"
Github uses "Conversation" now.
2014-06-01 22:45:10 +09:00
Naoki Takezoe
fb3ed70215 (refs #382)Exclude duplicated commits from applying issue comment 2014-06-01 21:47:45 +09:00
Naoki Takezoe
2fceeeee4e Merge pull request #386 from HairyFotr/patch-4
Small cleanup using static analysis
2014-05-31 17:26:23 +09:00
Naoki Takezoe
67102822e8 Update README.md 2014-05-31 13:17:21 +09:00
Naoki Takezoe
d00a0f1571 Update README.md 2014-05-31 13:16:53 +09:00
Naoki Takezoe
db5395ddbc Update version number to 2.0. 2014-05-31 10:38:31 +09:00
HairyFotr
7698f12112 Small cleanup using static analysis 2014-05-31 00:57:03 +02:00
Naoki Takezoe
1e8224536b (refs #383)Disable "New Issue" button in the new issue creation page. 2014-05-29 01:55:10 +09:00
Naoki Takezoe
a846c77c7e (refs #346)Add group members as collaborator when transfer repository to the group. 2014-05-25 18:14:53 +09:00
Tomofumi Tanaka
29812f4a82 (refs #375)Show merge commit diffs correctly 2014-05-20 21:26:51 +09:00
takezoe
a863951d97 Show diff for files other than markdown by "Preview" button 2014-05-19 00:29:29 +09:00
takezoe
146be677ba Hide "Edit" button if target is not head of the branch. 2014-05-19 00:03:24 +09:00
takezoe
03b5f7feb8 Fix title in file editing. 2014-05-18 23:36:13 +09:00
Tomofumi Tanaka
6d54361a6d Fix style broken in Firefox 2014-05-18 23:28:28 +09:00
Tomofumi Tanaka
f440421ed1 Repository url protocol label should be change
Like repository url.
2014-05-18 23:05:48 +09:00
takezoe
e57464fc5e Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-05-18 23:03:37 +09:00
takezoe
2a4b0f5ddb (refs #372)Use shift key instead if ctrl key to select region 2014-05-18 22:58:16 +09:00
Tomofumi Tanaka
bb66e2201f Fix width styles
* issue updation title and content
* issue new comment
* pull request new comment
2014-05-18 21:44:25 +09:00
takezoe
4dc60e887f (refs #373)Preview markdown in the text file editor. 2014-05-18 21:05:51 +09:00
takezoe
f6eb2e2dc8 (refs #230)Fix markdown preview styles 2014-05-18 16:04:38 +09:00
takezoe
9ecc10ab21 (refs #372)Select lines by clicking line number in blob view 2014-05-18 15:35:50 +09:00
Naoki Takezoe
d7037a43c6 bugfix 2014-05-16 17:06:57 +09:00
Naoki Takezoe
2471b8dfe0 bugfix 2014-05-16 14:31:00 +09:00
Tomofumi Tanaka
0430cb49f9 Fix typo 2014-05-13 23:53:23 +09:00
takezoe
7811926779 (refs #367)Redirect if forked repository already exists 2014-05-12 23:56:42 +09:00
takezoe
9bb66a4297 Fix broken layout in pull request detail page 2014-05-12 23:39:32 +09:00
takezoe
70772f0d74 (refs #364)Keep links which start with '#' 2014-05-11 13:13:16 +09:00
takezoe
728b00e4c3 Fix readme styles 2014-05-11 13:08:32 +09:00
takezoe
97008ef984 Merge branch 'ui-refreshing'
Conflicts:
	src/main/twirl/index.scala.html
2014-05-11 03:04:31 +09:00
takezoe
6b86406e94 Add hover icons for header links 2014-05-11 02:58:29 +09:00
takezoe
4252c364a4 Fix commit id style in file list 2014-05-11 02:57:49 +09:00
takezoe
4f4bc0321b Fix file list style in repository viewer 2014-05-11 02:55:43 +09:00
takezoe
6ecabe4588 Fix header style 2014-05-11 02:42:03 +09:00
takezoe
93fa8484c5 Fix header button size 2014-05-11 02:23:51 +09:00
takezoe
ff2e55e82c Fix h1-h6 styles 2014-05-11 02:01:46 +09:00
takezoe
259637ce3c Fix wiki styles 2014-05-11 01:38:12 +09:00
takezoe
743b9b759a Fix button style in blob page 2014-05-10 23:17:16 +09:00
takezoe
73ba0b348b Add branch switcher 2014-05-10 23:10:36 +09:00
takezoe
e93769cc81 Fix header and side-menu styles 2014-05-10 22:51:14 +09:00
Naoki Takezoe
68f9739eed Merge pull request #365 from selesy/announce_ssh
Update the features list in README.md
2014-05-10 21:54:22 +09:00
Steve Moyer
c3d25b7a71 Update the features list in README.md
The version 1.12 announces the addition of the SSH protocol for repository access, but the feature list still states "Public / Private Git repository (http access only)".
2014-05-10 08:06:57 -04:00
takezoe
aaa582ff1a Fix header style in wiki pages 2014-05-10 20:35:49 +09:00
takezoe
debc798aec Side-menu is completed! 2014-05-10 14:44:19 +09:00
takezoe
6042f0e1e0 Add active icons for side-menu 2014-05-08 07:46:32 +09:00
takezoe
e10d02f45c Apply mouse hover style 2014-05-08 07:30:35 +09:00
takezoe
aebf4ff728 Remove invalid char 2014-05-08 02:15:30 +09:00
Naoki Takezoe
1a2e89c9ed Disable bootstrap tooltip because icon link blinks. 2014-05-07 10:52:31 +09:00
Naoki Takezoe
e10e2748b9 Fix issue and pull request creation form styles 2014-05-07 10:37:51 +09:00
takezoe
f422936e34 Add <div class="container"> to pages outside of repository. 2014-05-06 21:32:40 +09:00
takezoe
4e87f21405 Revert style for fork count 2014-05-06 12:41:45 +09:00
takezoe
dc2d79b16c Remove unnecessary parts and styles. 2014-05-06 03:21:31 +09:00
takezoe
88a3100563 Fix tab style 2014-05-06 02:28:33 +09:00
takezoe
8d3433a0e7 Add icon for repository resttings 2014-05-06 02:17:41 +09:00
takezoe
0fe30e5629 Rename header.scala.html to menu.scala.html 2014-05-06 02:17:11 +09:00
takezoe
ea1e9037c4 Folding side-menu 2014-05-06 01:01:43 +09:00
takezoe
24feeb17be Add icons for new UI 2014-05-05 22:06:18 +09:00
takezoe
6a7fc55572 Global navigation moves to side menu. 2014-05-05 22:05:41 +09:00
takezoe
cf047a8cee Migrate: add extension to files which are attached to issue 2014-05-04 18:45:58 +09:00
takezoe
896420f8dc Disable AceEditor for non text files 2014-05-04 17:58:47 +09:00
Tomofumi Tanaka
619f72d929 Add link for image file
* Render image tag with link tag on issue and wiki
* Correct response content-type of attached image on issue
2014-05-03 00:56:36 +09:00
Tomofumi Tanaka
dc21e8388e Improve update pull request query
commitIdFrom and commitIdTo columns update by one query.
2014-05-02 19:55:49 +09:00
takezoe
642e8bbb7c Fix #358 2014-05-01 01:39:19 +09:00
Naoki Takezoe
3ee4143235 Move duplicated JavaScript and CSS for diff to common files 2014-04-30 10:55:42 +09:00
Naoki Takezoe
c136823170 Fix comment 2014-04-30 10:45:23 +09:00
Naoki Takezoe
92631fbfcf Fix JavaScript and CSS for attachment area 2014-04-30 10:28:46 +09:00
Naoki Takezoe
5a1b1a4485 Update README.md 2014-04-29 18:27:51 +09:00
Naoki Takezoe
3e82534c78 Update README.md 2014-04-29 17:40:53 +09:00
Tomofumi Tanaka
dd694d27b5 (refs #324)Update commitIdFrom when pullrequest branch is updated 2014-04-29 17:24:51 +09:00
takezoe
1900aefe32 Modify message in avatar uploader 2014-04-29 17:24:16 +09:00
takezoe
2fe6b8c1e7 Fix TestCase 2014-04-29 17:02:16 +09:00
takezoe
ecfaa0247a (refs #12)Fix styles 2014-04-29 16:42:40 +09:00
takezoe
9a0cc9e043 Fix baseURL to baseUrl 2014-04-29 15:52:24 +09:00
takezoe
b0360db105 Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-04-29 15:44:58 +09:00
takezoe
0f9c95c15a baseUrl calculation is concentrated to SystemSettings 2014-04-29 15:43:41 +09:00
shimamoto
8efd1da7e6 Merge branch 'fileupload' 2014-04-29 14:42:25 +09:00
shimamoto
52ebba43d5 (refs #12) Modified the response status when the file does not exist. 2014-04-29 13:54:06 +09:00
shimamoto
790eee7443 (refs #12) Add acceptedFiles options. 2014-04-29 13:41:50 +09:00
shimamoto
9f325290e8 (refs #12) Modified from path to baseURL. 2014-04-29 11:53:13 +09:00
shimamoto
93bf0a9a47 (refs #12) Modified to use the helper of the attachment. 2014-04-29 04:21:48 +09:00
shimamoto
bdd0af21a9 (refs #12) Created the helper of the attachment function. 2014-04-29 04:19:40 +09:00
Naoki Takezoe
aae5fe387b Update README.md 2014-04-29 02:52:02 +09:00
takezoe
257c5aef51 Merge branch 'ace-editor' 2014-04-29 02:43:01 +09:00
takezoe
3cae337487 (refs #13)New file icon 2014-04-29 02:41:19 +09:00
takezoe
779df30ec8 Fix #355 2014-04-29 02:03:28 +09:00
Naoki Takezoe
5609507991 (refs #13)Fix link target of cancel button 2014-04-28 10:45:13 +09:00
Naoki Takezoe
1c24090c14 (refs #13)Fix real-time validation for filename and Add line wrap mode switcher 2014-04-28 10:39:32 +09:00
takezoe
7da2c650d2 (refs #13)Bug fix 2014-04-28 01:33:13 +09:00
shimamoto
27fa9df2ee (refs #12) Implemented the process of saving image. 2014-04-27 21:08:31 +09:00
shimamoto
63c4e12259 (refs #12) Change the Markdown notation of images. 2014-04-27 19:11:14 +09:00
shimamoto
1f66670819 (refs #12) Implemented the upload area in preview.hrml. 2014-04-27 16:59:54 +09:00
shimamoto
a7b4f8de8d (refs #12) Replaced the latest version of the dropzone. 2014-04-27 16:55:48 +09:00
takezoe
ad0d57fbf9 Merge remote-tracking branch 'origin/master' 2014-04-27 02:11:39 +09:00
takezoe
cfc594805b (refs #348)Upgrade MINA to 0.11.0 2014-04-27 02:11:10 +09:00
Naoki Takezoe
52461e673c Merge pull request #351 from ysuganuma/Fix308
Fix #308
2014-04-27 02:06:27 +09:00
takezoe
a97edb7ef5 (refs #13)File editing (add, edit and delete) in repository viewer is available. 2014-04-27 02:02:36 +09:00
Yasuhito Suganuma
7a1c872861 Fix #308 2014-04-25 23:49:17 +09:00
Naoki Takezoe
0e5591017a Refactoring 2014-04-25 10:38:34 +09:00
takezoe
a104157c9a (refs #13)Disable commit button if content is not modified. 2014-04-25 00:09:34 +09:00
Naoki Takezoe
ad244adbfa (refs #13)Commit from AceEditor is available. 2014-04-24 21:49:30 +09:00
takezoe
3721b328a6 (refs #13)Implementing file editing on the repository viewer 2014-04-24 07:42:00 +09:00
takezoe
dd688f48b7 (refs #13)Implementing file editing on the repository viewer 2014-04-24 02:19:29 +09:00
shimamoto
296a0b2124 Removed FileUploadControllerBase(in the middle of improving file
upload).
2014-04-23 02:02:19 +09:00
takezoe
b9cc46e5ef (refs #13)Trying to embed ace editor into repository viewer. 2014-04-22 07:24:50 +09:00
Naoki Takezoe
375211fc30 Remove unused code 2014-04-21 16:04:25 +09:00
Naoki Takezoe
b8b59f9dcd Fix TestCase 2014-04-18 11:10:12 +09:00
Naoki Takezoe
6760ff34ef Adjust avatar icon style 2014-04-18 10:49:48 +09:00
Naoki Takezoe
c5de7811c4 Display large icons at the user repository list page 2014-04-18 10:33:45 +09:00
takezoe
82ef5457b0 Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-04-18 07:54:10 +09:00
takezoe
d558476cd2 (refs #327)Fix baseURL and host in Context 2014-04-18 07:53:09 +09:00
takezoe
644701d995 (refs #327)Add atom feed of the specified user 2014-04-18 07:23:52 +09:00
takezoe
1382d59206 (refs #327)Fix icon position for global recent activities feed 2014-04-18 07:11:49 +09:00
takezoe
b60e2c07c7 (refs #327)Move feed.scala.xml to helper package because it breaks compilation by overriding xml package 2014-04-18 07:04:31 +09:00
takezoe
86f0307633 Merge branch 'feedactivities' of https://github.com/kaakaa/gitbucket into kaakaa-feedactivities 2014-04-18 06:44:27 +09:00
Naoki Takezoe
1db891a771 (refs #345)Fix an error for users who are authenticated with mail address through LDAP in push over HTTP. 2014-04-15 19:26:09 +09:00
Naoki Takezoe
c9fa3291f5 Fix TestCase 2014-04-11 13:23:36 +09:00
Naoki Takezoe
e0f1658120 Use retro as default Gravator image 2014-04-11 12:12:24 +09:00
Naoki Takezoe
da105b7180 Merge remote-tracking branch 'origin/master' 2014-04-11 11:58:51 +09:00
Naoki Takezoe
9c4f7cc530 Fix message for empty repository 2014-04-11 11:57:16 +09:00
takezoe
d7eef8bd25 (refs #343)Add drop COMMIT_LOG table statement in migration for 1.13 2014-04-11 07:40:07 +09:00
Tomofumi Tanaka
7b7c0e1eee Fix getAllcommitIds bug in empty repository 2014-04-11 01:35:31 +09:00
Naoki Takezoe
2ae7798591 (refs #343)Apply fix to ssh pushing also 2014-04-10 17:55:58 +09:00
Naoki Takezoe
3f76453f34 (refs #343)Retrieve all commit id from Git repository in pre commit hook instead of COMMIT_LOG table 2014-04-10 17:42:25 +09:00
Naoki Takezoe
8fbbe7f31e (refs #337)Fix JavaScript not found problem in JBoss/WildFly 2014-04-10 11:25:39 +09:00
takezoe
92a43b4f99 Merge branch 'remove-asciidoc' 2014-04-10 07:44:02 +09:00
Naoki Takezoe
c128086778 (refs #335)Revert account updating in LDAP authentication 2014-04-08 16:34:01 +09:00
Naoki Takezoe
cc4fb8bf79 (refs #335)Add Scaladoc 2014-04-08 16:24:53 +09:00
Naoki Takezoe
c3ac0f3d9f (refs #335)Fix account updating in LDAP authentication 2014-04-08 16:21:10 +09:00
Naoki Takezoe
dfa4816633 (refs #288)Once remove AsciiDoc support 2014-04-08 10:13:07 +09:00
Tomofumi Tanaka
06978a4fc4 (refs #340)Add public key validation 2014-04-07 22:56:13 +09:00
Tomofumi Tanaka
3a2ecf6896 Fix bug #340 2014-04-07 22:53:31 +09:00
Naoki Takezoe
b357d52ec5 (refs #335)Fix LDAP authentication 2014-04-07 21:08:13 +09:00
Naoki Takezoe
f8b6b1ebf8 Merge pull request #288 from lefou/asciidoctorj
Support AsciiDoc markup for all files + Plain text Readme's
2014-04-04 13:09:45 +09:00
takezoe
91bd9d1111 (refs #335)Oops, fix substring condition 2014-04-03 08:55:50 +09:00
takezoe
1ec825050d (refs #335)Use string before '@' of mail address if user name is mail address in LDAP authentication. 2014-04-03 08:47:42 +09:00
takezoe
a6a08d13e9 (refs #335)Use string before '@' of mail address if user name is mail address in LDAP authentication. 2014-04-03 06:54:56 +09:00
takezoe
9a47c4a990 Fix #334 2014-04-03 00:32:39 +09:00
Naoki Takezoe
5063294177 Merge pull request #332 from mslinn/patch-1
Update README.md
2014-04-02 09:36:00 +09:00
Mike Slinn
b14917e2c6 Update README.md 2014-03-31 22:43:29 -07:00
takezoe
c1bbec2a1c Use logger instead of println 2014-04-01 04:53:32 +09:00
takezoe
6227a4643a Fix #331 2014-04-01 04:47:08 +09:00
takezoe
5d3365a944 Fix #328 2014-03-31 00:39:30 +09:00
takezoe
84ac2974fb Skip issue id extraction if the repository has no issues 2014-03-30 20:52:39 +09:00
Tobias Roeser
c9a1515d1f Merge branch 'master' into asciidoctorj
Conflicts:
	src/main/scala/app/RepositoryViewerController.scala
2014-03-29 13:53:23 +01:00
kaakaa
3bff6a1949 Implement atom feeds 2014-03-24 23:06:37 +09:00
Tobias Roeser
10a40bfcaf Removed commented out code. 2014-03-05 09:28:13 +01:00
Tobias Roeser
af397ba150 Fix page-relative links, e.g. in TOC. 2014-03-05 09:12:03 +01:00
Tobias Roeser
4276c8f23e Support relative links in asciidoc files. 2014-03-04 16:43:56 +01:00
Tobias Roeser
9e1352c8b1 Enabled rendering of renderable files in blob view. 2014-03-04 16:43:50 +01:00
Tobias Roeser
3db3bf1b74 Rewrite relative links to reflect the base url of the repo. 2014-03-04 11:38:56 +01:00
Tobias Roeser
0311359922 Merge branch 'master' into asciidoctorj 2014-03-02 09:15:57 +01:00
Tobias Roeser
89601305f6 Merge branch 'master' into asciidoctorj
Conflicts:
	src/main/twirl/repo/files.scala.html
2014-02-28 09:42:50 +01:00
Tobias Roeser
4600b5a3bf Enabled rendering of page document title. 2014-02-28 09:36:04 +01:00
Tobias Roeser
cbf615d699 Support plain text readme files (with .txt or no extension). 2014-02-26 16:13:39 +01:00
Tobias Roeser
97b1a0090d Initial support for rendering asciidoc files. 2014-02-26 15:14:39 +01:00
Tobias Roeser
9078aa6d08 Added asciidoctorj dependency. 2014-02-26 13:53:50 +01:00
Tobias Roeser
8677146a8d Show the correct name of the readme file (instead of showing always README.md). 2014-02-26 12:12:49 +01:00
402 changed files with 273487 additions and 6097 deletions

View File

@@ -1,12 +1,15 @@
GitBucket GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/badge/icon)](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
========= =========
GitBucket is the easily installable Github clone written with Scala. GitBucket is the easily installable Github clone written with Scala.
Features
--------
The current version of GitBucket provides a basic features below: The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http access only) - Public / Private Git repository (http and ssh access)
- Repository viewer (some advanced features such as online file editing are not implemented) - Repository viewer and online file editing
- Repository search (Code and Issues) - Repository search (Code and Issues)
- Wiki - Wiki
- Issues - Issues
@@ -20,7 +23,6 @@ The current version of GitBucket provides a basic features below:
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!
- File editing in repository viewer
- Comment for the changeset - Comment for the changeset
- Network graph - Network graph
- Statistics - Statistics
@@ -51,6 +53,24 @@ To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored i
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo) For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X ### Mac OS X
#### Installing Via Homebrew
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
To have launchd start gitbucket at login:
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
Then to load gitbucket now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
Or, if you don't want/need launchctl, you can just run:
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
==> Summary
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
#### Manual Installation
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/` On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to Run the following commands in `Terminal` to
@@ -60,6 +80,22 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.1 - 6 Jul 2014
- Upgrade to Slick 2.0 from 1.9
- Base part of the plug-in system is merged
- Many bug fix and improvements
### 2.0 - 31 May 2014
- Modern Github UI
- Preview in AceEditor
- Select lines by clicking line number in blob view
### 1.13 - 29 Apr 2014
- Direct file editing in the repository viewer using AceEditor
- File attachment for issues
- Atom feed of user activity
- Fix some bugs
### 1.12 - 29 Mar 2014 ### 1.12 - 29 Mar 2014
- SSH repository access is available - SSH repository access is available
- Allow users can create and management their groups - Allow users can create and management their groups

View File

@@ -50,8 +50,8 @@
</target> </target>
<target name="rename" depends="embed"> <target name="rename" depends="embed">
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war" <move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/> tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target> </target>
<target name="all" depends="rename"> <target name="all" depends="rename">

View File

@@ -2,6 +2,7 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
@@ -16,7 +17,16 @@
inkscape:version="0.48.4 r9939" inkscape:version="0.48.4 r9939"
sodipodi:docname="icons.svg"> sodipodi:docname="icons.svg">
<defs <defs
id="defs4" /> id="defs4">
<linearGradient
id="linearGradient4044"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4046" />
</linearGradient>
</defs>
<sodipodi:namedview <sodipodi:namedview
id="base" id="base"
pagecolor="#ffffff" pagecolor="#ffffff"
@@ -24,15 +34,15 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1.4" inkscape:zoom="0.7"
inkscape:cx="450.21999" inkscape:cx="482.58197"
inkscape:cy="97.51519" inkscape:cy="-83.92636"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1-9" inkscape:current-layer="layer1-9"
showgrid="false" showgrid="false"
inkscape:window-width="1366" inkscape:window-width="1366"
inkscape:window-height="706" inkscape:window-height="715"
inkscape:window-x="1912" inkscape:window-x="-8"
inkscape:window-y="-8" inkscape:window-y="-8"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:snap-global="false" inkscape:snap-global="false"
@@ -978,6 +988,716 @@
id="path4310" id="path4310"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="ccscc" /> sodipodi:nodetypes="ccscc" />
<path
id="path2991-7-1-4"
transform="translate(668.66057,1115.0272)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-5-8"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,711.41194,1163.4493)" />
<rect
id="rect2995-0-2-8"
y="1378.4849"
x="916.58545"
height="99.396141"
width="20.706863"
style="fill:#b3b3b3;stroke:none;stroke-width:0.93666755999999995" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 884.0251,1366.2678 -64.6851,-36.2114 10.70013,55.9569 53.98497,-19.7455 z"
id="rect4046-3-4"
inkscape:connector-curvature="0" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 873.36878,1359.3959 -43.65605,-24.4345 6.99871,38.1562 36.65734,-13.7217 z"
id="rect4046-5"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#b3b3b3;stroke-width:13.63542366;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 1162.2186,1316.0972 c -0.2525,22.2049 -0.505,44.4098 -0.7575,66.6147 3.3299,0.032 6.6599,0.063 9.9898,0.095 -2.3515,2.3672 -4.703,4.7345 -7.0544,7.1018 31.3741,31.374 62.7482,62.7482 94.1223,94.1223 23.3412,-23.3412 46.6824,-46.6824 70.0236,-70.0236 -31.3741,-31.3899 -62.7483,-62.7798 -94.1224,-94.1697 -2.6197,2.6356 -5.2395,5.2711 -7.8593,7.9067 0.032,-3.6298 0.063,-7.2596 0.095,-10.8894 -21.4789,-0.2525 -42.9579,-0.505 -64.4368,-0.7575 z"
id="rect3075-11"
inkscape:connector-curvature="0" />
<rect
id="rect2995-0-2-8-6"
y="899.99463"
x="-1417.3273"
height="99.396141"
width="20.706863"
style="fill:#b3b3b3;stroke:none;stroke-width:0.93666755999999995"
transform="matrix(0,-1,1,0,0,0)" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.37699986;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 1172.5522,1326.6744 c -0.1922,16.8985 -0.3844,33.7973 -0.5765,50.6959 2.5342,0.024 5.0684,0.047 7.6026,0.072 -1.7896,1.8014 -3.5792,3.6031 -5.3686,5.4047 23.8766,23.8766 47.7533,47.7533 71.63,71.6301 17.7636,-17.7634 35.5269,-35.5268 53.2902,-53.2902 -23.8766,-23.8888 -47.7534,-47.7775 -71.6302,-71.6662 -1.9936,2.0058 -3.9873,4.0114 -5.9811,6.0172 0.024,-2.7624 0.047,-5.5247 0.072,-8.2872 -16.3463,-0.1921 -32.6925,-0.3843 -49.0386,-0.5764 z"
id="rect3075-11-7"
inkscape:connector-curvature="0" />
<path
sodipodi:type="arc"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:6.57334423;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none"
id="path3100-2"
sodipodi:cx="700"
sodipodi:cy="812.36218"
sodipodi:rx="10"
sodipodi:ry="10"
d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-667.98357,1217.7251)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#b3b3b3;stroke-width:10.80681515;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4114"
width="45.086407"
height="62.401226"
x="-133.16023"
y="1850.2394"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
<path
id="path2991-7-6"
transform="translate(1090.5728,-207.2632)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#a0a0a0;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-8"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#808080;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,1133.3242,-158.84107)" />
<rect
id="rect2995-0-8"
y="10.829478"
x="1332.5247"
height="99.221687"
width="29.189819"
style="fill:#a0a0a0;stroke:#ffffff;stroke-width:1.11112404000000000;fill-opacity:1" />
<rect
id="rect2997-9-2"
y="129.62337"
x="1332.7828"
height="26.258072"
width="29.724136"
style="fill:#a0a0a0;stroke:#ffffff;stroke-width:0.57680577000000000;fill-opacity:1" />
<g
id="g4284-1"
transform="translate(670.07237,-816.24186)"
style="stroke:#a0a0a0;stroke-opacity:1">
<path
sodipodi:nodetypes="czcczcc"
inkscape:connector-curvature="0"
id="rect4201-26"
d="m 568.37427,1080.8464 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43273,8.6574 40.43273,8.6574 l 0,141.4674 c 0,0 -20.97035,-7.7215 -40.43273,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:14.36538028999999900;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" />
<rect
y="1108.1473"
x="597.4068"
height="5.4857273"
width="55.265846"
id="rect4203-0"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="1142.7776"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-4"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="1176.1093"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-3-9"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="czc"
inkscape:connector-curvature="0"
id="path4245-4"
d="m 563.55369,1233.6274 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29117,14.7566 46.29117,14.7566"
style="fill:#b3b3b3;stroke:#a0a0a0;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<g
transform="matrix(-1.0032405,0,0,1,1329.8708,99.560238)"
id="g4277-6"
style="stroke:#a0a0a0;stroke-opacity:1">
<path
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:14.36538124000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="m 519.67634,980.83663 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43272,8.6574 40.43272,8.6574 l 0,141.46737 c 0,0 -20.97034,-7.7215 -40.43272,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
id="rect4201-2-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcczcc" />
<rect
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4203-21-3"
width="55.26585"
height="5.4857273"
x="548.70886"
y="1008.1376" />
<rect
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4203-2-6-6"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1042.7678" />
<rect
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4203-2-3-8-2"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1076.0995" />
<path
style="fill:#b3b3b3;stroke:#a0a0a0;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 514.85576,1133.6176 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29116,14.7566 46.29116,14.7566"
id="path4245-5-4"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czc" />
</g>
</g>
<path
inkscape:connector-curvature="0"
id="path3850-1-1"
d="m 1409.5992,670.87038 0,-128.57724 c 0,0 1.8599,-15.30681 -16.7384,-15.30681 -18.5984,0 -51.1454,0 -51.1454,0"
style="fill:none;stroke:#a0a0a0;stroke-width:22.72570610000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="547.80316"
x="1294.749"
height="104.27072"
width="3.2554622"
id="rect3818-4-7"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(1.0049237,0,0,0.61497516,944.16607,536.33294)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-4"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,942.63054,386.00935)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-0"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1056.9547,536.43446)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-2-9"
style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-4-4"
d="m 1369.3146,490.2451 0,70.69144 -45.5889,-32.13462 z"
style="fill:#a0a0a0;stroke:#a0a0a0;stroke-width:0.83335358000000004px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953"
width="15.304287"
height="97.947441"
x="1474.2273"
y="-367.14282"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-8"
width="15.304287"
height="97.947441"
x="-281.45197"
y="-1573.058"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82"
width="15.304287"
height="97.947441"
x="-412.46057"
y="-1617.4926"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82-4"
width="15.304287"
height="97.947441"
x="-1617.2937"
y="306.0546"
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)" />
<g
id="g4016"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,3107.8871,982.01044)">
<rect
transform="scale(-1,-1)"
y="-1119.1083"
x="-1370.8767"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1-4"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:16.74562263;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="scale(-1,-1)"
y="-1207.0963"
x="-1358.6217"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7-0"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:9.11664104;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
transform="matrix(1.5972925,0,0,1.509886,-99.098035,-27.987625)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:7.14799976;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.1933397,0,0,1.5429659,269.38527,-37.350485)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226-9"
style="fill:#ffffff;fill-opacity:1;stroke:none"
sodipodi:type="arc" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect4027"
width="73.460579"
height="107.13"
x="1722.2299"
y="-207.2868"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<g
id="g4022"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,1095.262,-713.65443)">
<rect
transform="scale(-1,-1)"
y="-1121.4039"
x="-1505.5544"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:16.74562263;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="scale(-1,-1)"
y="-1222.4006"
x="-1494.0955"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:9.11664104;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="rect3182"
d="m 1469.5507,1220.8203 20.5952,52.7426 20.6425,-52.7426 -2.0832,-5.35 -37.0713,0 -2.0832,5.35 z"
style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:7.02416945;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
<path
id="path2991-7-6-1"
transform="translate(1482.3625,-199.43254)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#3c3c3c;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-8-7"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#3c3c80;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,1525.1139,-151.0104)" />
<rect
id="rect2995-0-8-4"
y="18.660131"
x="1724.3145"
height="99.221687"
width="29.189819"
style="fill:#3c3c3c;fill-opacity:1;stroke:#ffffff;stroke-width:1.11112404000000000" />
<rect
id="rect2997-9-2-0"
y="137.45401"
x="1724.5726"
height="26.258072"
width="29.724136"
style="fill:#3c3c3c;fill-opacity:1;stroke:#ffffff;stroke-width:0.57680577000000000" />
<g
id="g4284-1-9"
transform="translate(1061.8621,-808.41119)"
style="stroke:#3c3c3c;stroke-opacity:1">
<path
sodipodi:nodetypes="czcczcc"
inkscape:connector-curvature="0"
id="rect4201-26-4"
d="m 568.37427,1080.8464 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43273,8.6574 40.43273,8.6574 l 0,141.4674 c 0,0 -20.97035,-7.7215 -40.43273,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:14.36538028999999900;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" />
<rect
y="1108.1473"
x="597.4068"
height="5.4857273"
width="55.265846"
id="rect4203-0-8"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="1142.7776"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-4-8"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="1176.1093"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-3-9-2"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
sodipodi:nodetypes="czc"
inkscape:connector-curvature="0"
id="path4245-4-4"
d="m 563.55369,1233.6274 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29117,14.7566 46.29117,14.7566"
style="fill:#b3b3b3;stroke:#3c3c3c;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<g
transform="matrix(-1.0032405,0,0,1,1329.8708,99.560238)"
id="g4277-6-5"
style="stroke:#3c3c3c;stroke-opacity:1">
<path
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:14.36538124000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="m 519.67634,980.83663 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43272,8.6574 40.43272,8.6574 l 0,141.46737 c 0,0 -20.97034,-7.7215 -40.43272,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
id="rect4201-2-0-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcczcc" />
<rect
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4203-21-3-1"
width="55.26585"
height="5.4857273"
x="548.70886"
y="1008.1376" />
<rect
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4203-2-6-6-7"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1042.7678" />
<rect
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4203-2-3-8-2-1"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1076.0995" />
<path
style="fill:#b3b3b3;stroke:#3c3c3c;stroke-width:19.63722609999999900;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 514.85576,1133.6176 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29116,14.7566 46.29116,14.7566"
id="path4245-5-4-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czc" />
</g>
</g>
<path
inkscape:connector-curvature="0"
id="path3850-1-1-5"
d="m 1801.3889,678.70099 0,-128.5772 c 0,0 1.8599,-15.3068 -16.7384,-15.3068 -18.5984,0 -51.1454,0 -51.1454,0"
style="fill:none;stroke:#3c3c3c;stroke-width:22.72570610000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
y="555.63385"
x="1686.5388"
height="104.27072"
width="3.2554622"
id="rect3818-4-7-2"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1335.9558,544.16359)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-4-7"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1334.4203,393.83999)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-0-6"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,1448.7444,544.26509)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-2-9-1"
style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-4-4-4"
d="m 1761.1043,498.07579 0,70.6914 -45.5889,-32.1346 z"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:0.83335358000000004px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-2"
width="15.304287"
height="97.947441"
x="1756.8015"
y="-638.64288"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-8-3"
width="15.304287"
height="97.947441"
x="-552.95203"
y="-1855.6323"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82-2"
width="15.304287"
height="97.947441"
x="-683.96063"
y="-1900.0669"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3953-82-4-2"
width="15.304287"
height="97.947441"
x="-1899.8679"
y="577.55469"
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)" />
<g
id="g4138">
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
y="2055.4602"
x="403.84506"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1-4-6"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:16.74562263000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
y="1967.4722"
x="416.10007"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7-0-8"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:9.11664103999999930;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
transform="matrix(-1.1294564,1.1294564,-1.0676506,-1.0676506,3589.5398,939.55844)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226-5"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:7.14799976000000030;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(-0.84381859,0.84381859,-1.0910416,-1.0910416,3335.6033,1206.736)"
d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
sodipodi:ry="26.263966"
sodipodi:rx="26.263966"
sodipodi:cy="838.20984"
sodipodi:cx="910.14746"
id="path3226-9-7"
style="fill:#ffffff;fill-opacity:1;stroke:none"
sodipodi:type="arc" />
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
y="-476.95105"
x="2003.8865"
height="107.13"
width="73.460579"
id="rect4027-6"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<rect
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)"
y="429.19318"
x="-2057.9661"
height="105.80523"
width="33.87508"
id="rect3953-82-4-1-8"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:16.74562263000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)"
y="328.19647"
x="-2046.5071"
height="113.43421"
width="9.3650599"
id="rect3953-82-4-1-7-9"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:9.11664103999999930;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="rect3182-2"
d="m 1662.9307,1196.5558 -22.7317,51.8577 51.8911,-22.6982 2.31,-5.2561 -26.2134,-26.2134 -5.256,2.31 z"
style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:7.02416944999999960;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
<path
id="path2991-7-1-4-1"
transform="translate(-154.10522,1432.0357)"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#bebeff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-5-8-7"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-111.35384,1480.4578)" />
<rect
id="rect2995-0-2-8-4"
y="1695.4933"
x="93.81971"
height="99.396141"
width="20.706863"
style="fill:#bebefa;stroke:none;fill-opacity:1" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 61.259358,1683.2763 -64.6850999,-36.2114 10.7001,55.9569 53.9849999,-19.7455 z"
id="rect4046-3-4-0"
inkscape:connector-curvature="0" />
<path
style="fill:#bebeff;stroke:#bebeff;stroke-width:1.98877633000000010;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1;stroke-opacity:1"
d="m 50.602958,1676.4044 -43.6559999,-24.4345 6.9986999,38.1562 36.6573,-13.7217 z"
id="rect4046-5-9"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#bebefa;stroke-width:13.63542366000000100;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 339.45286,1633.1057 c -0.2525,22.2049 -0.505,44.4098 -0.7575,66.6147 3.3299,0.032 6.6598,0.063 9.9898,0.095 -2.3515,2.3672 -4.703,4.7345 -7.0544,7.1018 31.3741,31.374 62.7482,62.7482 94.1222,94.1223 23.3413,-23.3412 46.6825,-46.6824 70.0237,-70.0236 -31.3741,-31.3899 -62.7483,-62.7798 -94.1224,-94.1697 -2.6197,2.6356 -5.2395,5.2711 -7.8593,7.9067 0.032,-3.6298 0.063,-7.2596 0.095,-10.8894 -21.4789,-0.2525 -42.9579,-0.505 -64.4368,-0.7575 z"
id="rect3075-11-4"
inkscape:connector-curvature="0" />
<rect
id="rect2995-0-2-8-6-8"
y="77.228889"
x="-1734.3357"
height="99.396141"
width="20.706863"
style="fill:#bebefa;stroke:none;fill-opacity:1"
transform="matrix(0,-1,1,0,0,0)" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.37699986;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 349.78646,1643.6829 c -0.1922,16.8985 -0.3844,33.7973 -0.5765,50.6959 2.5342,0.024 5.0684,0.047 7.6026,0.072 -1.7896,1.8014 -3.5792,3.6031 -5.3686,5.4047 23.8766,23.8766 47.7533,47.7533 71.63,71.6301 17.7636,-17.7634 35.5269,-35.5268 53.2902,-53.2902 -23.8766,-23.8888 -47.7534,-47.7775 -71.6302,-71.6662 -1.9936,2.0058 -3.9873,4.0114 -5.9811,6.0172 0.024,-2.7624 0.047,-5.5247 0.072,-8.2872 -16.3463,-0.1921 -32.6925,-0.3843 -49.0386,-0.5764 z"
id="rect3075-11-7-8"
inkscape:connector-curvature="0" />
<path
sodipodi:type="arc"
style="fill:#ffffff;stroke:#bebefa;stroke-width:6.57334423000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none"
id="path3100-2-2"
sodipodi:cx="700"
sodipodi:cy="812.36218"
sodipodi:rx="10"
sodipodi:ry="10"
d="m 710,812.36218 a 10,10 0 1 1 -20,0 10,10 0 1 1 20,0 z"
transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect4114-4"
width="45.086407"
height="62.401226"
x="-939.10236"
y="1492.6151"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
<path
style="fill:none;stroke:#bebeff;stroke-width:25.84518814000000100;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 655.10691,1790.494 c 0,0 3.44333,-28.5633 47.63498,-35.4849 15.10377,-2.3655 48.7968,-8.2798 48.7968,-42.5816"
id="path3207-5"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-9.2946303"
sodipodi:nodetypes="csc"
inkscape:transform-center-y="2.9369479e-005" />
<rect
y="1676.2623"
x="652.97418"
height="104.27072"
width="3.2554622"
id="rect3818-4-8-4-5"
style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7-8-1"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8-7"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8-2-1"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" />
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -15,6 +15,7 @@ object MyBuild extends Build {
"gitbucket", "gitbucket",
file("."), file("."),
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq( settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
sourcesInBase := false,
organization := Organization, organization := Organization,
name := Name, name := Name,
version := Version, version := Version,
@@ -36,9 +37,11 @@ object MyBuild extends Build {
"org.apache.commons" % "commons-compress" % "1.5", "org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1", "org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3", "org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.10.0", "org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "1.0.1", "com.typesafe.slick" %% "slick" % "2.0.2",
"org.mozilla" % "rhino" % "1.7R4",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.3.173", "com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",

View File

@@ -1,135 +1,135 @@
CREATE TABLE ACCOUNT( CREATE TABLE ACCOUNT(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
MAIL_ADDRESS VARCHAR(100) NOT NULL, MAIL_ADDRESS VARCHAR(100) NOT NULL,
PASSWORD VARCHAR(40) NOT NULL, PASSWORD VARCHAR(40) NOT NULL,
ADMINISTRATOR BOOLEAN NOT NULL, ADMINISTRATOR BOOLEAN NOT NULL,
URL VARCHAR(200), URL VARCHAR(200),
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP NOT NULL,
LAST_LOGIN_DATE TIMESTAMP LAST_LOGIN_DATE TIMESTAMP
); );
CREATE TABLE REPOSITORY( CREATE TABLE REPOSITORY(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
PRIVATE BOOLEAN NOT NULL, PRIVATE BOOLEAN NOT NULL,
DESCRIPTION TEXT, DESCRIPTION TEXT,
DEFAULT_BRANCH VARCHAR(100), DEFAULT_BRANCH VARCHAR(100),
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP NOT NULL,
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE COLLABORATOR( CREATE TABLE COLLABORATOR(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
COLLABORATOR_NAME VARCHAR(100) NOT NULL COLLABORATOR_NAME VARCHAR(100) NOT NULL
); );
CREATE TABLE ISSUE( CREATE TABLE ISSUE(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
OPENED_USER_NAME VARCHAR(100) NOT NULL, OPENED_USER_NAME VARCHAR(100) NOT NULL,
MILESTONE_ID INT, MILESTONE_ID INT,
ASSIGNED_USER_NAME VARCHAR(100), ASSIGNED_USER_NAME VARCHAR(100),
TITLE TEXT NOT NULL, TITLE TEXT NOT NULL,
CONTENT TEXT, CONTENT TEXT,
CLOSED BOOLEAN NOT NULL, CLOSED BOOLEAN NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL UPDATED_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE ISSUE_ID( CREATE TABLE ISSUE_ID(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL ISSUE_ID INT NOT NULL
); );
CREATE TABLE ISSUE_COMMENT( CREATE TABLE ISSUE_COMMENT(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
COMMENT_ID INT AUTO_INCREMENT, COMMENT_ID INT AUTO_INCREMENT,
ACTION VARCHAR(10), ACTION VARCHAR(10),
COMMENTED_USER_NAME VARCHAR(100) NOT NULL, COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
CONTENT TEXT NOT NULL, CONTENT TEXT NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL UPDATED_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE LABEL( CREATE TABLE LABEL(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
LABEL_ID INT AUTO_INCREMENT, LABEL_ID INT AUTO_INCREMENT,
LABEL_NAME VARCHAR(100) NOT NULL, LABEL_NAME VARCHAR(100) NOT NULL,
COLOR CHAR(6) NOT NULL COLOR CHAR(6) NOT NULL
); );
CREATE TABLE ISSUE_LABEL( CREATE TABLE ISSUE_LABEL(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
LABEL_ID INT NOT NULL LABEL_ID INT NOT NULL
); );
CREATE TABLE MILESTONE( CREATE TABLE MILESTONE(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
MILESTONE_ID INT AUTO_INCREMENT, MILESTONE_ID INT AUTO_INCREMENT,
TITLE VARCHAR(100) NOT NULL, TITLE VARCHAR(100) NOT NULL,
DESCRIPTION TEXT, DESCRIPTION TEXT,
DUE_DATE TIMESTAMP, DUE_DATE TIMESTAMP,
CLOSED_DATE TIMESTAMP CLOSED_DATE TIMESTAMP
); );
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME); ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS); ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_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_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); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID); ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID); ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID); ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
INSERT INTO ACCOUNT ( INSERT INTO ACCOUNT (
USER_NAME, USER_NAME,
MAIL_ADDRESS, MAIL_ADDRESS,
PASSWORD, PASSWORD,
ADMINISTRATOR, ADMINISTRATOR,
URL, URL,
REGISTERED_DATE, REGISTERED_DATE,
UPDATED_DATE, UPDATED_DATE,
LAST_LOGIN_DATE LAST_LOGIN_DATE
) VALUES ( ) VALUES (
'root', 'root',
'root@localhost', 'root@localhost',
'dc76e9f0c0006e8f919e0c515c66dbba3982f785', 'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
true, true,
'https://github.com/takezoe/gitbucket', 'https://github.com/takezoe/gitbucket',
SYSDATE, SYSDATE,
SYSDATE, SYSDATE,
NULL NULL
); );

View File

@@ -0,0 +1 @@
DROP TABLE COMMIT_LOG;

View File

@@ -1,6 +1,6 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter} import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
import app._ import app._
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider //import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._ import org.scalatra._
import javax.servlet._ import javax.servlet._
import java.util.EnumSet import java.util.EnumSet
@@ -10,6 +10,8 @@ class ScalatraBootstrap extends LifeCycle {
// Register TransactionFilter and BasicAuthenticationFilter at first // Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter) context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter) context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
@@ -28,7 +30,6 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new IssuesController, "/*") context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*") context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*") context.mount(new RepositorySettingsController, "/*")
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
// Create GITBUCKET_HOME directory if it does not exist // Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(_root_.util.Directory.GitBucketHome) val dir = new java.io.File(_root_.util.Directory.GitBucketHome)

View File

@@ -5,6 +5,8 @@ import util._
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._
import ssh.SshUtil
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.i18n.Messages import org.scalatra.i18n.Messages
@@ -49,7 +51,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val sshKeyForm = mapping( val sshKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))), "title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim(label("Key" , text(required))) "publicKey" -> trim(label("Key" , text(required, validPublicKey)))
)(SshKeyForm.apply) )(SshKeyForm.apply)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
@@ -111,13 +113,19 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val members = getGroupMembers(account.userName) val members = getGroupMembers(account.userName)
_root_.account.html.repositories(account, _root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName), if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)), getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
} }
} }
} getOrElse NotFound } getOrElse NotFound
} }
get("/:userName.atom") {
val userName = params("userName")
contentType = "application/atom+xml; type=feed"
helper.xml.feed(getActivitiesByUser(userName, true))
}
get("/:userName/_avatar"){ get("/:userName/_avatar"){
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image => getAccountByUserName(userName).flatMap(_.image).map { image =>
@@ -284,8 +292,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){ LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){ if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
@@ -347,62 +355,43 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName){ if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
// redirect to the repository // redirect to the repository if repository already exists
redirect(s"/${repository.owner}/${repository.name}") redirect(s"/${loginUserName}/${repository.name}")
} else { } else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) => // Insert to the database at first
// redirect to the repository val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
redirect(s"/${owner}/${name}") val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository( createRepository(
repositoryName = repository.name, repositoryName = repository.name,
userName = loginUserName, userName = loginUserName,
description = repository.repository.description, description = repository.repository.description,
isPrivate = repository.repository.isPrivate, isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName), originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName), originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name), parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner) parentUserName = Some(repository.owner)
) )
// Insert default labels // Insert default labels
insertDefaultLabels(loginUserName, repository.name) insertDefaultLabels(loginUserName, repository.name)
// clone repository actually // clone repository actually
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name), getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name)) getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository // Create Wiki repository
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name), getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name)) getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id // Record activity
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git => recordForkActivity(repository.owner, repository.name, loginUserName)
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch => // redirect to the repository
JGitUtil.getCommitLog(git, branch) match { redirect(s"/${loginUserName}/${repository.name}")
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
} }
} }
}) })
@@ -435,4 +424,11 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}) None else Some("Must select one manager at least.") }) None else Some("Must select one manager at least.")
} }
} }
private def validPublicKey: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match {
case Some(_) => None
case None => Some("Key is invalid.")
}
}
} }

View File

@@ -9,10 +9,9 @@ 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 org.apache.commons.io.FileUtils
import model.Account import model._
import service.{SystemSettingsService, AccountService} import service.{SystemSettingsService, AccountService}
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest} import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._ import org.scalatra.i18n._
@@ -139,9 +138,10 @@ abstract class ControllerBase extends ScalatraFilter
*/ */
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
lazy val path = settings.baseUrl.getOrElse(request.getServletContext.getContextPath) val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length) val baseUrl = settings.baseUrl(request)
val host = new java.net.URL(baseUrl).getHost
/** /**
* Get object from cache. * Get object from cache.
@@ -163,7 +163,7 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
/** /**
* Base trait for controllers which manages account information. * Base trait for controllers which manages account information.
*/ */
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase { trait AccountManagementControllerBase extends ControllerBase {
self: AccountService => self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
@@ -174,9 +174,9 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
} }
} else { } else {
fileId.map { fileId => fileId.map { fileId =>
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get) val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
FileUtils.moveFile( FileUtils.moveFile(
getTemporaryFile(fileId), new java.io.File(getTemporaryDir(session.getId), fileId),
new java.io.File(getUserUploadDir(userName), filename) new java.io.File(getUserUploadDir(userName), filename)
) )
updateAvatarImage(userName, Some(filename)) updateAvatarImage(userName, Some(filename))
@@ -196,28 +196,3 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
} }
} }
/**
* 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] =
session.getAndRemove[String](Keys.Session.Upload(fileId))
}

View File

@@ -1,109 +1,109 @@
package app package app
import service._ import service._
import util.{UsersAuthenticator, Keys} import util.{UsersAuthenticator, Keys}
import util.Implicits._ import util.Implicits._
class DashboardController extends DashboardControllerBase class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase { trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly { get("/dashboard/issues/repos")(usersOnly {
searchIssues("all") searchIssues("all")
}) })
get("/dashboard/issues/assigned")(usersOnly { get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned") searchIssues("assigned")
}) })
get("/dashboard/issues/created_by")(usersOnly { get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by") searchIssues("created_by")
}) })
get("/dashboard/pulls")(usersOnly { get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
get("/dashboard/pulls/owned")(usersOnly { get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
get("/dashboard/pulls/public")(usersOnly { get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None) searchPullRequests("not_created_by", None)
}) })
get("/dashboard/pulls/for/:owner/:repository")(usersOnly { get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository"))) searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
}) })
private def searchIssues(filter: String) = { private def searchIssues(filter: String) = {
import IssuesService._ import IssuesService._
// condition // condition
val condition = session.putAndGet(Keys.Session.DashboardIssues, val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
) )
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
// //
dashboard.html.issues( dashboard.html.issues(
issues.html.listparts( issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*), searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page, page,
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*), countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*), countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
condition), condition),
countIssue(condition, Map.empty, false, repositories: _*), countIssue(condition, Map.empty, false, repositories: _*),
countIssue(condition, Map("assigned" -> userName), false, repositories: _*), countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
countIssue(condition, Map("created_by" -> userName), false, repositories: _*), countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
countIssueGroupByRepository(condition, filterUser, false, repositories: _*), countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
condition, condition,
filter) filter)
} }
private def searchPullRequests(filter: String, repository: Option[String]) = { private def searchPullRequests(filter: String, repository: Option[String]) = {
import IssuesService._ import IssuesService._
import PullRequestService._ import PullRequestService._
// condition // condition
val condition = session.putAndGet(Keys.Session.DashboardPulls, { val condition = session.putAndGet(Keys.Session.DashboardPulls, {
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
}.copy(repo = repository)) }.copy(repo = repository))
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository( val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*) IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
dashboard.html.pulls( dashboard.html.pulls(
pulls.html.listparts( pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*), searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
page, page,
countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*), countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*), countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
condition, condition,
None, None,
false), false),
getPullRequestCountGroupByUser(condition.state == "closed", userName, None), getPullRequestCountGroupByUser(condition.state == "closed", None, None),
getRepositoryNamesOfUser(userName).map { RepoName => getRepositoryNamesOfUser(userName).map { RepoName =>
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0)) (userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
}.sortBy(_._3).reverse, }.sortBy(_._3).reverse,
condition, condition,
filter) filter)
} }
} }

View File

@@ -1,31 +1,44 @@
package app package app
import _root_.util.{Keys, FileUtil} import util.{Keys, FileUtil}
import util.ControlUtil._ import util.ControlUtil._
import util.Directory._
import org.scalatra._ import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport} import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
/** /**
* Provides Ajax based file upload functionality. * Provides Ajax based file upload functionality.
* *
* This servlet saves uploaded file as temporary file and returns the unique id. * This servlet saves uploaded file.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/ */
class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase { class FileUploadController extends ScalatraServlet with FileUploadSupport {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){ post("/image"){
fileParams.get("file") match { execute { (file, fileId) =>
case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId => FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get) session += Keys.Session.Upload(fileId) -> file.name
session += Keys.Session.Upload(fileId) -> file.name
Ok(fileId)
}
case None => BadRequest
} }
} }
} post("/image/:owner/:repository"){
execute { (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
}
}
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)
Ok(fileId)
}
case _ => BadRequest
}
}

View File

@@ -1,80 +1,86 @@
package app package app
import util._ import util._
import service._ import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import service._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with UsersAuthenticator class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
case class SignInForm(userName: String, password: String)
case class SignInForm(userName: String, password: String)
val form = mapping(
"userName" -> trim(label("Username", text(required))), val form = mapping(
"password" -> trim(label("Password", text(required))) "userName" -> trim(label("Username", text(required))),
)(SignInForm.apply) "password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
get("/"){
val loginAccount = context.loginAccount get("/"){
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, baseUrl), html.index(getRecentActivities(),
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil) getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
) loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
} )
}
get("/signin"){
val redirect = params.get("redirect") get("/signin"){
if(redirect.isDefined && redirect.get.startsWith("/")){ val redirect = params.get("redirect")
flash += Keys.Flash.Redirect -> redirect.get if(redirect.isDefined && redirect.get.startsWith("/")){
} flash += Keys.Flash.Redirect -> redirect.get
html.signin() }
} html.signin()
}
post("/signin", form){ form =>
authenticate(context.settings, form.userName, form.password) match { post("/signin", form){ form =>
case Some(account) => signin(account) authenticate(context.settings, form.userName, form.password) match {
case None => redirect("/signin") case Some(account) => signin(account)
} case None => redirect("/signin")
} }
}
get("/signout"){
session.invalidate get("/signout"){
redirect("/") session.invalidate
} redirect("/")
}
/**
* Set account information into HttpSession and redirect. get("/activities.atom"){
*/ contentType = "application/atom+xml; type=feed"
private def signin(account: model.Account) = { helper.xml.feed(getRecentActivities())
session.setAttribute(Keys.Session.LoginAccount, account) }
updateLastLoginDate(account.userName)
/**
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => * Set account information into HttpSession and redirect.
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){ */
redirect("/") private def signin(account: model.Account) = {
} else { session.setAttribute(Keys.Session.LoginAccount, account)
redirect(redirectUrl) updateLastLoginDate(account.userName)
}
}.getOrElse { flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
redirect("/") if(redirectUrl.stripSuffix("/") == request.getContextPath){
} redirect("/")
} } else {
redirect(redirectUrl)
/** }
* JSON API for collaborator completion. }.getOrElse {
* redirect("/")
* TODO Move to other controller? }
*/ }
get("/_user/proposals")(usersOnly {
contentType = formats("json") /**
org.json4s.jackson.Serialization.write( * JSON API for collaborator completion.
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray) *
) * 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

@@ -1,392 +1,403 @@
package app package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import IssuesService._ import IssuesService._
import util._ import util._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue import model.Issue
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService 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 AccountService with LabelsService with MilestonesService with ActivityService self: IssuesService with RepositoryService with AccountService 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]) 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))),
"content" -> trim(optional(text())), "content" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())), "assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())), "milestoneId" -> trim(optional(number())),
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
val issueEditForm = mapping( val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueEditForm.apply) )(IssueEditForm.apply)
val commentForm = mapping( val commentForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(label("Comment", text(required))) "content" -> trim(label("Comment", text(required)))
)(CommentForm.apply) )(CommentForm.apply)
val issueStateForm = mapping( val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _) searchIssues("all", _)
}) })
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly { get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _) searchIssues("assigned", _)
}) })
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly { get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _) searchIssues("created_by", _)
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getIssue(owner, name, issueId) map { getIssue(owner, name, issueId) map {
issues.html.issue( issues.html.issue(
_, _,
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
repository) repository)
} getOrElse NotFound } getOrElse NotFound
} }
}) })
get("/:owner/:repository/issues/new")(readableUsersOnly { repository => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create( issues.html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name), getMilestones(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
repository) repository)
} }
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
val writable = hasWritePermission(owner, name, context.loginAccount) val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
// insert issue // insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content, 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 // insert labels
if(writable){ if(writable){
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label => labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId) registerIssueLabel(owner, name, issueId, label.labelId)
} }
} }
} }
} }
// record activity // record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title) recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// extract references and create refer comment // extract references and create refer comment
getIssue(owner, name, issueId.toString).foreach { issue => getIssue(owner, name, issueId.toString).foreach { issue =>
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
} }
// notifications // notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}") Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
} }
redirect(s"/${owner}/${name}/issues/${issueId}") 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) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
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)){
// update issue // update issue
updateIssue(owner, name, issue.issueId, form.title, form.content) updateIssue(owner, name, issue.issueId, form.title, form.content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
redirect(s"/${owner}/${name}/issues/_data/${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) =>
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${ redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) => handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${ redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound } getOrElse NotFound
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
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"/${owner}/${name}/issue_comments/_data/${comment.commentId}") redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
} }
}) })
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
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)){
Ok(deleteComment(comment.commentId)) Ok(deleteComment(comment.commentId))
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
} }
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { x => getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => issues.html.editissue( case t if t == "html" => issues.html.editissue(
x.title, x.content, x.issueId, x.userName, x.repositoryName) x.title, x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
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) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
getComment(repository.owner, repository.name, params("id")) map { x => getComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => issues.html.editcomment( case t if t == "html" => issues.html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName) x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
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) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId => defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
} }
}) })
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId => defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
} }
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
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, milestoneId("milestoneId")) updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false) issues.milestones.html.progress(openCount + closeCount, closeCount, false)
} getOrElse NotFound } getOrElse NotFound
} getOrElse Ok() } getOrElse Ok()
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action => defining(params.get("value")){ action =>
executeBatch(repository) { executeBatch(repository) {
handleComment(_, None, repository)( _ => action) handleComment(_, None, repository)( _ => action)
} }
} }
}) })
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
params("value").toIntOpt.map{ labelId => params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { issueId => executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId) registerIssueLabel(repository.owner, repository.name, issueId, labelId)
} }
} }
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
defining(assignedUserName("value")){ value => defining(assignedUserName("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value) updateAssignedUserName(repository.owner, repository.name, _, value)
} }
} }
}) })
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
defining(milestoneId("value")){ value => defining(milestoneId("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value) updateMilestoneId(repository.owner, repository.name, _, value)
} }
} }
}) })
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) (Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if(dir.exists && dir.isDirectory) =>
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName contentType = FileUtil.getMimeType(file.getName)
file
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { }
params("checked").split(',') map(_.toInt) foreach execute case _ => None
redirect(s"/${repository.owner}/${repository.name}/issues") }) getOrElse NotFound
} })
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
StringUtil.extractIssueId(message).foreach { issueId => val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
if(getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
fromIssue.issueId + ":" + fromIssue.title, "refer") 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 createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) StringUtil.extractIssueId(message).foreach { issueId =>
(getAction: model.Issue => Option[String] = if(getIssue(owner, repository, issueId).isDefined){
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
fromIssue.issueId + ":" + fromIssue.title, "refer")
defining(repository.owner, repository.name){ case (owner, name) => }
val userName = context.loginAccount.get.userName }
}
getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) = /**
getAction(issue) * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
.collect { */
case "close" => true -> (Some("close") -> private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) (getAction: model.Issue => Option[String] =
case "reopen" => false -> (Some("reopen") -> p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
Some(recordReopenIssueActivity _))
} defining(repository.owner, repository.name){ case (owner, name) =>
.map { case (closed, t) => val userName = context.loginAccount.get.userName
updateClosed(owner, name, issueId, closed)
t getIssue(owner, name, issueId.toString) map { issue =>
} val (action, recordActivity) =
.getOrElse(None -> None) getAction(issue)
.collect {
val commentId = content case "close" => true -> (Some("close") ->
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
.getOrElse ( action.get.capitalize -> action.get ) case "reopen" => false -> (Some("reopen") ->
match { Some(recordReopenIssueActivity _))
case (content, action) => createComment(owner, name, userName, issueId, content, action) }
} .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
// record activity t
content foreach { }
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) .getOrElse(None -> None)
(owner, name, userName, issueId, _)
} val commentId = content
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get )
// extract references and create refer comment match {
content.map { content => case (content, action) => createComment(owner, name, userName, issueId, content, action)
createReferComment(owner, name, issue, content) }
}
// record activity
// notifications content foreach {
Notifier() match { (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
case f => (owner, name, userName, issueId, _)
content foreach { }
f.toNotify(repository, issueId, _){ recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") // extract references and create refer comment
} content.map { content =>
} createReferComment(owner, name, issue, content)
action foreach { }
f.toNotify(repository, issueId, _){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}") // notifications
} Notifier() match {
} case f =>
} content foreach {
f.toNotify(repository, issueId, _){
issue -> commentId Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
} if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
} }
} }
action foreach {
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { f.toNotify(repository, issueId, _){
defining(repository.owner, repository.name){ case (owner, repoName) => Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
val filterUser = Map(filter -> params.getOrElse("userName", "")) }
val page = IssueSearchCondition.page(request) }
val sessionKey = Keys.Session.Issues(owner, repoName) }
// retrieve search condition issue -> commentId
val condition = session.putAndGet(sessionKey, }
if(request.hasQueryString) IssueSearchCondition(request) }
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) }
)
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
issues.html.list( defining(repository.owner, repository.name){ case (owner, repoName) =>
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), val filterUser = Map(filter -> params.getOrElse("userName", ""))
page, val page = IssueSearchCondition.page(request)
(getCollaborators(owner, repoName) :+ owner).sorted, val sessionKey = Keys.Session.Issues(owner, repoName)
getMilestones(owner, repoName),
getLabels(owner, repoName), // retrieve search condition
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), val condition = session.putAndGet(sessionKey,
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), if(request.hasQueryString) IssueSearchCondition(request)
countIssue(condition, Map.empty, false, owner -> repoName), else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), )
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser), issues.html.list(
condition, searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
filter, page,
repository, (getCollaborators(owner, repoName) :+ owner).sorted,
hasWritePermission(owner, repoName, context.loginAccount)) getMilestones(owner, repoName),
} getLabels(owner, repoName),
} countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
} countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}
}

View File

@@ -3,6 +3,7 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.CollaboratorsAuthenticator import util.CollaboratorsAuthenticator
import util.Implicits._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
@@ -53,7 +54,7 @@ trait LabelsControllerBase extends ControllerBase {
*/ */
private def labelName: Constraint = new Constraint(){ private def labelName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(!value.matches("^[^,]+$")){ if(value.contains(',')){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.") Some(s"${name} starts with invalid character.")
@@ -62,4 +63,4 @@ trait LabelsControllerBase extends ControllerBase {
} }
} }
} }

View File

@@ -13,7 +13,6 @@ import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._ import service.IssuesService._
import service.PullRequestService._ import service.PullRequestService._
import util.JGitUtil.DiffInfo import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
@@ -100,7 +99,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.mergeguide( pulls.html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
pullreq, pullreq,
s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
} }
} getOrElse NotFound } getOrElse NotFound
}) })
@@ -111,7 +110,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){ if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setBranchNames(branchName).call() git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
} }
} }
@@ -124,7 +123,7 @@ trait PullRequestsControllerBase extends ControllerBase {
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){ LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) => getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git => using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close. // mark issue as merged and close.
@@ -177,14 +176,8 @@ trait PullRequestsControllerBase extends ControllerBase {
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(owner, name, commit.id)){
insertCommitId(owner, name, commit.id)
}
}
// close issue by content of pull request // close issue by content of pull request
val defaultBranch = getRepository(owner, name, baseUrl).get.repository.defaultBranch val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){ if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit => commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
@@ -207,7 +200,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// notifications // notifications
Notifier().toNotify(repository, issueId, "merge"){ Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}") Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
} }
redirect(s"/${owner}/${name}/pull/${issueId}") redirect(s"/${owner}/${name}/pull/${issueId}")
@@ -220,7 +213,7 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare")(referrersOnly { forkedRepository => get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => { case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository => getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
using( using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
@@ -257,7 +250,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} }
}; };
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl) originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -266,7 +259,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit, val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch, originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch) forkedRepository.owner, forkedRepository.name, forkedBranch)
@@ -309,7 +302,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} }
}; };
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl) originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -362,7 +355,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// notifications // notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
} }
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
@@ -373,7 +366,7 @@ trait PullRequestsControllerBase extends ControllerBase {
*/ */
private def checkConflict(userName: String, repositoryName: String, branch: String, private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){ LockUtil.lock(s"${userName}/${repositoryName}"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}" val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}" val tmpRefName = s"refs/merge-check/${userName}/${branch}"
@@ -409,7 +402,7 @@ trait PullRequestsControllerBase extends ControllerBase {
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String, private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = { issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge") { LockUtil.lock(s"${userName}/${repositoryName}") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git => using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge // merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
@@ -438,24 +431,8 @@ trait PullRequestsControllerBase extends ControllerBase {
(defaultOwner, value) (defaultOwner, value)
} }
/**
* Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
*/
private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = { requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
using( using(
Git.open(getRepositoryDir(userName, repositoryName)), Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName)) Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
@@ -473,7 +450,6 @@ trait PullRequestsControllerBase extends ControllerBase {
(commits, diffs) (commits, diffs)
} }
}
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
@@ -489,7 +465,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.list( pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)), getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
userName, userName,
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),

View File

@@ -1,266 +1,272 @@
package app package app
import service._ import service._
import util.Directory._ import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator} import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import util.Implicits._
import org.apache.commons.io.FileUtils import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import org.scalatra.i18n.Messages import util.JGitUtil.CommitInfo
import service.WebHookService.WebHookPayload import jp.sf.amateras.scalatra.forms._
import util.JGitUtil.CommitInfo import org.apache.commons.io.FileUtils
import util.ControlUtil._ import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
class RepositorySettingsController extends RepositorySettingsControllerBase import util.ControlUtil._
with RepositoryService with AccountService with WebHookService import org.eclipse.jgit.api.Git
with OwnerAuthenticator with UsersAuthenticator
class RepositorySettingsController extends RepositorySettingsControllerBase
trait RepositorySettingsControllerBase extends ControllerBase { with RepositoryService with AccountService with WebHookService
self: RepositoryService with AccountService with WebHookService with OwnerAuthenticator with UsersAuthenticator
with OwnerAuthenticator with UsersAuthenticator =>
trait RepositorySettingsControllerBase extends ControllerBase {
// for repository options self: RepositoryService with AccountService with WebHookService
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) with OwnerAuthenticator with UsersAuthenticator =>
val optionsForm = mapping( // for repository options
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
"description" -> trim(label("Description" , optional(text()))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), val optionsForm = mapping(
"isPrivate" -> trim(label("Repository Type", boolean())) "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
)(OptionsForm.apply) "description" -> trim(label("Description" , optional(text()))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
// for collaborator addition "isPrivate" -> trim(label("Repository Type", boolean()))
case class CollaboratorForm(userName: String) )(OptionsForm.apply)
val collaboratorForm = mapping( // for collaborator addition
"userName" -> trim(label("Username", text(required, collaborator))) case class CollaboratorForm(userName: String)
)(CollaboratorForm.apply)
val collaboratorForm = mapping(
// for web hook url addition "userName" -> trim(label("Username", text(required, collaborator)))
case class WebHookForm(url: String) )(CollaboratorForm.apply)
val webHookForm = mapping( // for web hook url addition
"url" -> trim(label("url", text(required, webHook))) case class WebHookForm(url: String)
)(WebHookForm.apply)
val webHookForm = mapping(
// for transfer ownership "url" -> trim(label("url", text(required, webHook)))
case class TransferOwnerShipForm(newOwner: String) )(WebHookForm.apply)
val transferForm = mapping( // for transfer ownership
"newOwner" -> trim(label("New owner", text(required, transferUser))) case class TransferOwnerShipForm(newOwner: String)
)(TransferOwnerShipForm.apply)
val transferForm = mapping(
/** "newOwner" -> trim(label("New owner", text(required, transferUser)))
* Redirect to the Options page. )(TransferOwnerShipForm.apply)
*/
get("/:owner/:repository/settings")(ownerOnly { repository => /**
redirect(s"/${repository.owner}/${repository.name}/settings/options") * Redirect to the Options page.
}) */
get("/:owner/:repository/settings")(ownerOnly { repository =>
/** redirect(s"/${repository.owner}/${repository.name}/settings/options")
* Display the Options page. })
*/
get("/:owner/:repository/settings/options")(ownerOnly { /**
settings.html.options(_, flash.get("info")) * Display the Options page.
}) */
get("/:owner/:repository/settings/options")(ownerOnly {
/** settings.html.options(_, flash.get("info"))
* Save the repository options. })
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => /**
saveRepositoryOptions( * Save the repository options.
repository.owner, */
repository.name, post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
form.description, saveRepositoryOptions(
form.defaultBranch, repository.owner,
repository.repository.parentUserName.map { _ => repository.name,
repository.repository.isPrivate form.description,
} getOrElse form.isPrivate if(repository.branchList.isEmpty) "master" else form.defaultBranch,
) repository.repository.parentUserName.map { _ =>
// Change repository name repository.repository.isPrivate
if(repository.name != form.repositoryName){ } getOrElse form.isPrivate
// Update database )
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) // Change repository name
// Move git repository if(repository.name != form.repositoryName){
defining(getRepositoryDir(repository.owner, repository.name)){ dir => // Update database
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
} // Move git repository
// Move wiki repository defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) }
} // Move wiki repository
} defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
flash += "info" -> "Repository settings has been updated." FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") }
}) }
flash += "info" -> "Repository settings has been updated."
/** redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
* Display the Collaborators page. })
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => /**
settings.html.collaborators( * Display the Collaborators page.
getCollaborators(repository.owner, repository.name), */
getAccountByUserName(repository.owner).get.isGroupAccount, get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
repository) settings.html.collaborators(
}) getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
/** repository)
* Add the collaborator. })
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => /**
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ * Add the collaborator.
addCollaborator(repository.owner, repository.name, form.userName) */
} post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") if(!getAccountByUserName(repository.owner).get.isGroupAccount){
}) addCollaborator(repository.owner, repository.name, form.userName)
}
/** redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
* Add the collaborator. })
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => /**
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ * Add the collaborator.
removeCollaborator(repository.owner, repository.name, params("name")) */
} get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") if(!getAccountByUserName(repository.owner).get.isGroupAccount){
}) removeCollaborator(repository.owner, repository.name, params("name"))
}
/** redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
* Display the web hook page. })
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository => /**
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) * Display the web hook page.
}) */
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
/** settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
* Add the web hook URL. })
*/
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => /**
addWebHookURL(repository.owner, repository.name, form.url) * Add the web hook URL.
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") */
}) post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
addWebHookURL(repository.owner, repository.name, form.url)
/** redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
* Delete the web hook URL. })
*/
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => /**
deleteWebHookURL(repository.owner, repository.name, params("url")) * Delete the web hook URL.
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") */
}) get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHookURL(repository.owner, repository.name, params("url"))
/** redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
* Send the test request to registered web hook URLs. })
*/
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => /**
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => * Send the test request to registered web hook URLs.
import scala.collection.JavaConverters._ */
val commits = git.log get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
.add(git.getRepository.resolve(repository.repository.defaultBranch)) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
.setMaxCount(3) import scala.collection.JavaConverters._
.call.iterator.asScala.map(new CommitInfo(_)) val commits = git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
getWebHookURLs(repository.owner, repository.name) match { .setMaxCount(3)
case webHookURLs if(webHookURLs.nonEmpty) => .call.iterator.asScala.map(new CommitInfo(_))
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs, getWebHookURLs(repository.owner, repository.name) match {
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) case webHookURLs if(webHookURLs.nonEmpty) =>
} for(ownerAccount <- getAccountByUserName(repository.owner)){
case _ => callWebHook(repository.owner, repository.name, webHookURLs,
} WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
}
flash += "info" -> "Test payload deployed!" case _ =>
} }
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) flash += "info" -> "Test payload deployed!"
}
/** redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
* Display the danger zone. })
*/
get("/:owner/:repository/settings/danger")(ownerOnly { /**
settings.html.danger(_) * Display the danger zone.
}) */
get("/:owner/:repository/settings/danger")(ownerOnly {
/** settings.html.danger(_)
* Transfer repository ownership. })
*/
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => /**
// Change repository owner * Transfer repository ownership.
if(repository.owner != form.newOwner){ */
// Update database post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
renameRepository(repository.owner, repository.name, form.newOwner, repository.name) // Change repository owner
// Move git repository if(repository.owner != form.newOwner){
defining(getRepositoryDir(repository.owner, repository.name)){ dir => LockUtil.lock(s"${repository.owner}/${repository.name}"){
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) // Update database
} renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Move wiki repository // Move git repository
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
} }
} // Move wiki repository
redirect(s"/${form.newOwner}/${repository.name}") defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
}) FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
}
/** }
* Delete the repository. }
*/ redirect(s"/${form.newOwner}/${repository.name}")
post("/:owner/:repository/settings/delete")(ownerOnly { repository => })
deleteRepository(repository.owner, repository.name)
/**
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) * Delete the repository.
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) */
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}"){
redirect(s"/${repository.owner}") deleteRepository(repository.owner, repository.name)
})
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
/** FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
* Provides duplication check for web hook url. FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
*/ }
private def webHook: Constraint = new Constraint(){ redirect(s"/${repository.owner}")
override def validate(name: String, value: String, messages: Messages): Option[String] = })
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
} /**
* Provides duplication check for web hook url.
/** */
* Provides Constraint to validate the collaborator name. private def webHook: Constraint = new Constraint(){
*/ override def validate(name: String, value: String, messages: Messages): Option[String] =
private def collaborator: Constraint = new Constraint(){ getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
override def validate(name: String, value: String, messages: Messages): Option[String] = }
getAccountByUserName(value) match {
case None => Some("User does not exist.") /**
case Some(x) if(x.isGroupAccount) * Provides Constraint to validate the collaborator name.
=> Some("User does not exist.") */
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) private def collaborator: Constraint = new Constraint(){
=> Some("User can access this repository already.") override def validate(name: String, value: String, messages: Messages): Option[String] =
case _ => None getAccountByUserName(value) match {
} case None => Some("User does not exist.")
} case Some(x) if(x.isGroupAccount)
=> Some("User does not exist.")
/** case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
* Duplicate check for the rename repository name. => Some("User can access this repository already.")
*/ case _ => None
private def renameRepositoryName: Constraint = new Constraint(){ }
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = }
params.get("repository").filter(_ != value).flatMap { _ =>
params.get("owner").flatMap { userName => /**
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") * Duplicate check for the rename repository name.
} */
} private def renameRepositoryName: Constraint = new Constraint(){
} override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("repository").filter(_ != value).flatMap { _ =>
/** params.get("owner").flatMap { userName =>
* Provides Constraint to validate the repository transfer user. getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
*/ }
private def transferUser: Constraint = new Constraint(){ }
override def validate(name: String, value: String, messages: Messages): Option[String] = }
getAccountByUserName(value) match {
case None => Some("User does not exist.") /**
case Some(x) => if(x.userName == params("owner")){ * Provides Constraint to validate the repository transfer user.
Some("This is current repository owner.") */
} else { private def transferUser: Constraint = new Constraint(){
params.get("repository").flatMap { repositoryName => override def validate(name: String, value: String, messages: Messages): Option[String] =
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } getAccountByUserName(value) match {
} case None => Some("User does not exist.")
} case Some(x) => if(x.userName == params("owner")){
} Some("This is current repository owner.")
} } else {
params.get("repository").flatMap { repositoryName =>
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
}
}
}
}
} }

View File

@@ -1,8 +1,9 @@
package app package app
import _root_.util.JGitUtil.CommitInfo
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import _root_.util.ControlUtil._
import _root_.util._ import _root_.util._
import service._ import service._
import org.scalatra._ import org.scalatra._
@@ -12,17 +13,53 @@ import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream} import java.util.zip.{ZipEntry, ZipOutputStream}
import scala.Some import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
/** /**
* The repository viewer. * The repository viewer.
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator => self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class EditorForm(
branch: String,
path: String,
content: String,
message: Option[String],
charset: String,
newFileName: String,
oldFileName: Option[String]
)
case class DeleteForm(
branch: String,
path: String,
message: Option[String],
fileName: String
)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply)
val deleteForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"message" -> trim(label("Message", optional(text()))),
"fileName" -> trim(label("Filename", text(required)))
)(DeleteForm.apply)
/** /**
* Returns converted HTML from Markdown for preview. * Returns converted HTML from Markdown for preview.
*/ */
@@ -71,6 +108,68 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), None, form.content, form.charset,
form.message.getOrElse(s"Create ${form.newFileName}"))
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
}")
})
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName, form.content, form.charset,
if(form.oldFileName.exists(_ == form.newFileName)){
form.message.getOrElse(s"Update ${form.newFileName}")
} else {
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
})
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
}")
})
post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}"))
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
/** /**
* Displays the file content of the specified branch or commit. * Displays the file content of the specified branch or commit.
*/ */
@@ -80,19 +179,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).map { objectId =>
@scala.annotation.tailrec
def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
} map { objectId =>
if(raw){ if(raw){
// Download // Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
@@ -100,25 +187,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
bytes bytes
} }
} else { } else {
// Viewer repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
}
} else {
// image or large
JGitUtil.ContentInfo(viewer, None)
}
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
} }
} getOrElse NotFound } getOrElse NotFound
} }
@@ -164,7 +234,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){ if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setBranchNames(branchName).call() git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
} }
} }
@@ -185,7 +255,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val name = multiParams("splat").head val name = multiParams("splat").head
if(name.endsWith(".zip")){ if(name.endsWith(".zip")){
val revision = name.replaceFirst("\\.zip$", "") val revision = name.stripSuffix(".zip")
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists){ if(workDir.exists){
FileUtils.deleteDirectory(workDir) FileUtils.deleteDirectory(workDir)
@@ -234,29 +304,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getRepository( getRepository(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name), repository.repository.originRepositoryName.getOrElse(repository.name),
baseUrl), context.baseUrl),
getForkedRepositories( getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)), repository.repository.originRepositoryName.getOrElse(repository.name)),
repository) repository)
}) })
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = { private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst { val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst { } orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} orElse Some(path.split("/")(0)) get } getOrElse path.split("/")(0)
(id, path.substring(id.length).replaceFirst("^/", "")) (id, path.substring(id.length).stripPrefix("/"))
} }
private val readmeFiles = Seq("readme.md", "readme.markdown") private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
/** /**
* Provides HTML of the file list. * Provides HTML of the file list.
* *
* @param repository the repository information * @param repository the repository information
* @param revstr the branch name or commit id(optional) * @param revstr the branch name or commit id(optional)
* @param path the directory path (optional) * @param path the directory path (optional)
@@ -264,31 +334,99 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/ */
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){ if(repository.commitCount == 0){
repo.html.guide(repository) repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} else { } else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
//val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) //val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit // get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit => defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
// get files // get files
val files = JGitUtil.getFileList(git, revision, path) val files = JGitUtil.getFileList(git, revision, path)
val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown // process README.md or README.markdown
val readme = files.find { file => val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase) readmeFiles.contains(file.name.toLowerCase)
}.map { file => }.map { file =>
file -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( val path = (file.name :: parentPath.reverse).reverse
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
} }
repo.html.files(revision, repository, repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme) files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
} getOrElse NotFound } getOrElse NotFound
} }
} }
} }
private def commitFile(repository: service.RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String) = {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
LockUtil.lock(s"${repository.owner}/${repository.name}"){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val loginAccount = context.loginAccount.get
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(s"refs/heads/${branch}")
JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
// TODO invoke hook
}
}
}
private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@scala.annotation.tailrec
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
_getPathObjectId(path, treeWalk)
}
}
} }

View File

@@ -2,6 +2,7 @@ package app
import util._ import util._
import ControlUtil._ import ControlUtil._
import Implicits._
import service._ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._

View File

@@ -3,8 +3,13 @@ package app
import service.{AccountService, SystemSettingsService} import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import ssh.SshServer import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator with AccountService with AdminAuthenticator
@@ -47,6 +52,11 @@ trait SystemSettingsControllerBase extends ControllerBase {
} else Nil } else Nil
} }
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
admin.html.system(flash.get("info")) admin.html.system(flash.get("info"))
@@ -55,6 +65,10 @@ trait SystemSettingsControllerBase extends ControllerBase {
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form) saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){ if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(request.getServletContext, SshServer.start(request.getServletContext,
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
@@ -67,4 +81,104 @@ trait SystemSettingsControllerBase extends ControllerBase {
redirect("/admin/system") redirect("/admin/system")
}) })
// TODO Enable commented code to enable plug-in system
// get("/admin/plugins")(adminOnly {
// val installedPlugins = plugin.PluginSystem.plugins
// val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
// admin.plugins.html.installed(installedPlugins, updatablePlugins)
// })
//
// post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
// deletePlugins(form.pluginIds)
// installPlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
//
// post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
// deletePlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
//
// get("/admin/plugins/available")(adminOnly {
// val installedPlugins = plugin.PluginSystem.plugins
// val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
// admin.plugins.html.available(availablePlugins)
// })
//
// post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
// installPlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
// get("/admin/plugins/console")(adminOnly {
// admin.plugins.html.console()
// })
//
// post("/admin/plugins/console")(adminOnly {
// val script = request.getParameter("script")
// val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
// Ok(result)
// })
// TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = {
pluginIds.foreach { pluginId =>
plugin.PluginSystem.uninstall(pluginId)
val dir = new java.io.File(PluginHome, pluginId)
if(dir.exists && dir.isDirectory){
FileUtils.deleteQuietly(dir)
PluginSystem.uninstall(pluginId)
}
}
}
private def installPlugins(pluginIds: List[String]): Unit = {
val dir = getPluginCacheDir()
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(!pluginDir.exists){
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
}
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
} }

View File

@@ -4,10 +4,11 @@ import service._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import util.Directory._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.Directory._
class UserManagementController extends UserManagementControllerBase class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator

View File

@@ -4,10 +4,10 @@ import service._
import util._ import util._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import scala.Some
import java.util.ResourceBundle import java.util.ResourceBundle
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
@@ -36,7 +36,8 @@ 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, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
}) })
@@ -44,7 +45,8 @@ trait WikiControllerBase extends ControllerBase {
val pageName = StringUtil.urlDecode(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, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
}) })

View File

@@ -1,34 +1,40 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait AccountComponent { self: Profile =>
import profile.simple._
object Accounts extends Table[Account]("ACCOUNT") { import self._
def userName = column[String]("USER_NAME", O PrimaryKey)
def fullName = column[String]("FULL_NAME") lazy val Accounts = TableQuery[Accounts]
def mailAddress = column[String]("MAIL_ADDRESS")
def password = column[String]("PASSWORD") class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") {
def isAdmin = column[Boolean]("ADMINISTRATOR") val userName = column[String]("USER_NAME", O PrimaryKey)
def url = column[String]("URL") val fullName = column[String]("FULL_NAME")
def registeredDate = column[java.util.Date]("REGISTERED_DATE") val mailAddress = column[String]("MAIL_ADDRESS")
def updatedDate = column[java.util.Date]("UPDATED_DATE") val password = column[String]("PASSWORD")
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") val isAdmin = column[Boolean]("ADMINISTRATOR")
def image = column[String]("IMAGE") val url = column[String]("URL")
def groupAccount = column[Boolean]("GROUP_ACCOUNT") val registeredDate = column[java.util.Date]("REGISTERED_DATE")
def removed = column[Boolean]("REMOVED") val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _) val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
} val image = column[String]("IMAGE")
val groupAccount = column[Boolean]("GROUP_ACCOUNT")
case class Account( val removed = column[Boolean]("REMOVED")
userName: String, def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply)
fullName: String, }
mailAddress: String,
password: String, case class Account(
isAdmin: Boolean,
url: Option[String], userName: String,
registeredDate: java.util.Date, fullName: String,
updatedDate: java.util.Date, mailAddress: String,
lastLoginDate: Option[java.util.Date], password: String,
image: Option[String], isAdmin: Boolean,
isGroupAccount: Boolean, url: Option[String],
isRemoved: Boolean registeredDate: java.util.Date,
) updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date],
image: Option[String],
isGroupAccount: Boolean,
isRemoved: Boolean
)
}

View File

@@ -1,31 +1,29 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait ActivityComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate { lazy val Activities = TableQuery[Activities]
def activityId = column[Int]("ACTIVITY_ID", O AutoInc)
def activityUserName = column[String]("ACTIVITY_USER_NAME") class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
def activityType = column[String]("ACTIVITY_TYPE") val activityId = column[Int]("ACTIVITY_ID", O AutoInc)
def message = column[String]("MESSAGE") val activityUserName = column[String]("ACTIVITY_USER_NAME")
def additionalInfo = column[String]("ADDITIONAL_INFO") val activityType = column[String]("ACTIVITY_TYPE")
def activityDate = column[java.util.Date]("ACTIVITY_DATE") val message = column[String]("MESSAGE")
def * = activityId ~ userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate <> (Activity, Activity.unapply _) val additionalInfo = column[String]("ADDITIONAL_INFO")
def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId val activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
}
case class Activity(
userName: String,
repositoryName: String,
activityUserName: String,
activityType: String,
message: String,
additionalInfo: Option[String],
activityDate: java.util.Date,
activityId: Int = 0
)
} }
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,44 +1,47 @@
package model package model
import scala.slick.driver.H2Driver.simple._ protected[model] trait TemplateComponent { self: Profile =>
import profile.simple._
protected[model] trait BasicTemplate { self: Table[_] =>
def userName = column[String]("USER_NAME") trait BasicTemplate { self: Table[_] =>
def repositoryName = column[String]("REPOSITORY_NAME") val userName = column[String]("USER_NAME")
val repositoryName = column[String]("REPOSITORY_NAME")
def byRepository(owner: String, repository: String) =
(userName is owner.bind) && (repositoryName is repository.bind) def byRepository(owner: String, repository: String) =
(userName is owner.bind) && (repositoryName is repository.bind)
def byRepository(userName: Column[String], repositoryName: Column[String]) =
(this.userName is userName) && (this.repositoryName is repositoryName) def byRepository(userName: Column[String], repositoryName: Column[String]) =
} (this.userName is userName) && (this.repositoryName is repositoryName)
}
protected[model] trait IssueTemplate extends BasicTemplate { self: Table[_] =>
def issueId = column[Int]("ISSUE_ID") trait IssueTemplate extends BasicTemplate { self: Table[_] =>
val issueId = column[Int]("ISSUE_ID")
def byIssue(owner: String, repository: String, issueId: Int) =
byRepository(owner, repository) && (this.issueId is issueId.bind) def byIssue(owner: String, repository: String, issueId: Int) =
byRepository(owner, repository) && (this.issueId is issueId.bind)
def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
byRepository(userName, repositoryName) && (this.issueId is issueId) def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
} byRepository(userName, repositoryName) && (this.issueId is issueId)
}
protected[model] trait LabelTemplate extends BasicTemplate { self: Table[_] =>
def labelId = column[Int]("LABEL_ID") trait LabelTemplate extends BasicTemplate { self: Table[_] =>
val labelId = column[Int]("LABEL_ID")
def byLabel(owner: String, repository: String, labelId: Int) =
byRepository(owner, repository) && (this.labelId is labelId.bind) def byLabel(owner: String, repository: String, labelId: Int) =
byRepository(owner, repository) && (this.labelId is labelId.bind)
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
byRepository(userName, repositoryName) && (this.labelId is labelId) def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
} byRepository(userName, repositoryName) && (this.labelId is labelId)
}
protected[model] trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
def milestoneId = column[Int]("MILESTONE_ID") trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
val milestoneId = column[Int]("MILESTONE_ID")
def byMilestone(owner: String, repository: String, milestoneId: Int) =
byRepository(owner, repository) && (this.milestoneId is milestoneId.bind) def byMilestone(owner: String, repository: String, milestoneId: Int) =
byRepository(owner, repository) && (this.milestoneId is milestoneId.bind)
def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
byRepository(userName, repositoryName) && (this.milestoneId is milestoneId) def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
} byRepository(userName, repositoryName) && (this.milestoneId is milestoneId)
}
}

View File

@@ -1,17 +1,21 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object Collaborators extends Table[Collaborator]("COLLABORATOR") with BasicTemplate { lazy val Collaborators = TableQuery[Collaborators]
def collaboratorName = column[String]("COLLABORATOR_NAME")
def * = userName ~ repositoryName ~ collaboratorName <> (Collaborator, Collaborator.unapply _)
def byPrimaryKey(owner: String, repository: String, collaborator: String) = class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate {
byRepository(owner, repository) && (collaboratorName is collaborator.bind) val collaboratorName = column[String]("COLLABORATOR_NAME")
def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply)
def byPrimaryKey(owner: String, repository: String, collaborator: String) =
byRepository(owner, repository) && (collaboratorName is collaborator.bind)
}
case class Collaborator(
userName: String,
repositoryName: String,
collaboratorName: String
)
} }
case class Collaborator(
userName: String,
repositoryName: String,
collaboratorName: String
)

View File

@@ -1,16 +1,20 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait GroupMemberComponent { self: Profile =>
import profile.simple._
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") { lazy val GroupMembers = TableQuery[GroupMembers]
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
def userName = column[String]("USER_NAME", O PrimaryKey) class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") {
def isManager = column[Boolean]("MANAGER") val groupName = column[String]("GROUP_NAME", O PrimaryKey)
def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _) val userName = column[String]("USER_NAME", O PrimaryKey)
val isManager = column[Boolean]("MANAGER")
def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply)
}
case class GroupMember(
groupName: String,
userName: String,
isManager: Boolean
)
} }
case class GroupMember(
groupName: String,
userName: String,
isManager: Boolean
)

View File

@@ -1,41 +1,48 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait IssueComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTemplate { import self._
def * = userName ~ repositoryName ~ issueId
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) lazy val IssueId = TableQuery[IssueId]
} lazy val IssueOutline = TableQuery[IssueOutline]
lazy val Issues = TableQuery[Issues]
object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
def commentCount = column[Int]("COMMENT_COUNT") class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate {
def * = userName ~ repositoryName ~ issueId ~ commentCount def * = (userName, repositoryName, issueId)
} def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
def openedUserName = column[String]("OPENED_USER_NAME") class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate {
def assignedUserName = column[String]("ASSIGNED_USER_NAME") val commentCount = column[Int]("COMMENT_COUNT")
def title = column[String]("TITLE") def * = (userName, repositoryName, issueId, commentCount)
def content = column[String]("CONTENT") }
def closed = column[Boolean]("CLOSED")
def registeredDate = column[java.util.Date]("REGISTERED_DATE") class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate {
def updatedDate = column[java.util.Date]("UPDATED_DATE") val openedUserName = column[String]("OPENED_USER_NAME")
def pullRequest = column[Boolean]("PULL_REQUEST") val assignedUserName = column[String]("ASSIGNED_USER_NAME")
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _) val title = column[String]("TITLE")
val content = column[String]("CONTENT")
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) val closed = column[Boolean]("CLOSED")
} val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
case class Issue( val pullRequest = column[Boolean]("PULL_REQUEST")
userName: String, def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply)
repositoryName: String,
issueId: Int, def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
openedUserName: String, }
milestoneId: Option[Int],
assignedUserName: Option[String], case class Issue(
title: String, userName: String,
content: Option[String], repositoryName: String,
closed: Boolean, issueId: Int,
registeredDate: java.util.Date, openedUserName: String,
updatedDate: java.util.Date, milestoneId: Option[Int],
isPullRequest: Boolean) assignedUserName: Option[String],
title: String,
content: Option[String],
closed: Boolean,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
isPullRequest: Boolean)
}

View File

@@ -1,28 +1,34 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait IssueCommentComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate { import self._
def commentId = column[Int]("COMMENT_ID", O AutoInc)
def action = column[String]("ACTION") lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
def commentedUserName = column[String]("COMMENTED_USER_NAME") def autoInc = this returning this.map(_.commentId)
def content = column[String]("CONTENT") }
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE") class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _) val commentId = column[Int]("COMMENT_ID", O AutoInc)
val action = column[String]("ACTION")
def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId val commentedUserName = column[String]("COMMENTED_USER_NAME")
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind val content = column[String]("CONTENT")
} val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
case class IssueComment( def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
userName: String,
repositoryName: String, def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
issueId: Int, }
commentId: Int,
action: String, case class IssueComment(
commentedUserName: String, userName: String,
content: String, repositoryName: String,
registeredDate: java.util.Date, issueId: Int,
updatedDate: java.util.Date commentId: Int = 0,
) action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
)
}

View File

@@ -1,15 +1,19 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait IssueLabelComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object IssueLabels extends Table[IssueLabel]("ISSUE_LABEL") with IssueTemplate with LabelTemplate { lazy val IssueLabels = TableQuery[IssueLabels]
def * = userName ~ repositoryName ~ issueId ~ labelId <> (IssueLabel, IssueLabel.unapply _)
def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) = class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate {
byIssue(owner, repository, issueId) && (this.labelId is labelId.bind) def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
byIssue(owner, repository, issueId) && (this.labelId is labelId.bind)
}
case class IssueLabel(
userName: String,
repositoryName: String,
issueId: Int,
labelId: Int)
} }
case class IssueLabel(
userName: String,
repositoryName: String,
issueId: Int,
labelId: Int)

View File

@@ -1,34 +1,37 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait LabelComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object Labels extends Table[Label]("LABEL") with LabelTemplate { lazy val Labels = TableQuery[Labels]
def labelName = column[String]("LABEL_NAME")
def color = column[String]("COLOR")
def * = userName ~ repositoryName ~ labelId ~ labelName ~ color <> (Label, Label.unapply _)
def ins = userName ~ repositoryName ~ labelName ~ color class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate {
def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) override val labelId = column[Int]("LABEL_ID", O AutoInc)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId) val labelName = column[String]("LABEL_NAME")
} val color = column[String]("COLOR")
def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)
case class Label( def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
userName: String, def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
repositoryName: String,
labelId: Int,
labelName: String,
color: String){
val fontColor = {
val r = color.substring(0, 2)
val g = color.substring(2, 4)
val b = color.substring(4, 6)
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000"
} else {
"FFFFFF"
}
} }
} case class Label(
userName: String,
repositoryName: String,
labelId: Int = 0,
labelName: String,
color: String){
val fontColor = {
val r = color.substring(0, 2)
val g = color.substring(2, 4)
val b = color.substring(4, 6)
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000"
} else {
"FFFFFF"
}
}
}
}

View File

@@ -1,24 +1,29 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait MilestoneComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate { lazy val Milestones = TableQuery[Milestones]
def title = column[String]("TITLE")
def description = column[String]("DESCRIPTION")
def dueDate = column[java.util.Date]("DUE_DATE")
def closedDate = column[java.util.Date]("CLOSED_DATE")
def * = userName ~ repositoryName ~ milestoneId ~ title ~ description.? ~ dueDate.? ~ closedDate.? <> (Milestone, Milestone.unapply _)
def ins = userName ~ repositoryName ~ title ~ description.? ~ dueDate.? ~ closedDate.? class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate {
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId) val title = column[String]("TITLE")
val description = column[String]("DESCRIPTION")
val dueDate = column[java.util.Date]("DUE_DATE")
val closedDate = column[java.util.Date]("CLOSED_DATE")
def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply)
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
}
case class Milestone(
userName: String,
repositoryName: String,
milestoneId: Int = 0,
title: String,
description: Option[String],
dueDate: Option[java.util.Date],
closedDate: Option[java.util.Date])
} }
case class Milestone(
userName: String,
repositoryName: String,
milestoneId: Int,
title: String,
description: Option[String],
dueDate: Option[java.util.Date],
closedDate: Option[java.util.Date])

View File

@@ -0,0 +1,19 @@
package model
import slick.driver.JdbcProfile
trait Profile {
val profile: JdbcProfile
import profile.simple._
// java.util.Date Mapped Column Types
implicit val dateColumnType = MappedColumnType.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
}
}

View File

@@ -1,28 +1,32 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait PullRequestComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate { lazy val PullRequests = TableQuery[PullRequests]
def branch = column[String]("BRANCH")
def requestUserName = column[String]("REQUEST_USER_NAME")
def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
def requestBranch = column[String]("REQUEST_BRANCH")
def commitIdFrom = column[String]("COMMIT_ID_FROM")
def commitIdTo = column[String]("COMMIT_ID_TO")
def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _)
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId) class PullRequests(tag: Tag) extends Table[PullRequest](tag, "PULL_REQUEST") with IssueTemplate {
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId) val branch = column[String]("BRANCH")
val requestUserName = column[String]("REQUEST_USER_NAME")
val requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
val requestBranch = column[String]("REQUEST_BRANCH")
val commitIdFrom = column[String]("COMMIT_ID_FROM")
val commitIdTo = column[String]("COMMIT_ID_TO")
def * = (userName, repositoryName, issueId, branch, requestUserName, requestRepositoryName, requestBranch, commitIdFrom, commitIdTo) <> (PullRequest.tupled, PullRequest.unapply)
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
}
case class PullRequest(
userName: String,
repositoryName: String,
issueId: Int,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String
)
} }
case class PullRequest(
userName: String,
repositoryName: String,
issueId: Int,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String
)

View File

@@ -1,34 +1,39 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait RepositoryComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate { lazy val Repositories = TableQuery[Repositories]
def isPrivate = column[Boolean]("PRIVATE")
def description = column[String]("DESCRIPTION")
def defaultBranch = column[String]("DEFAULT_BRANCH")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
def originUserName = column[String]("ORIGIN_USER_NAME")
def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
def parentUserName = column[String]("PARENT_USER_NAME")
def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _)
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) class Repositories(tag: Tag) extends Table[Repository](tag, "REPOSITORY") with BasicTemplate {
val isPrivate = column[Boolean]("PRIVATE")
val description = column[String]("DESCRIPTION")
val defaultBranch = column[String]("DEFAULT_BRANCH")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
val originUserName = column[String]("ORIGIN_USER_NAME")
val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
val parentUserName = column[String]("PARENT_USER_NAME")
val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?) <> (Repository.tupled, Repository.unapply)
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
case class Repository(
userName: String,
repositoryName: String,
isPrivate: Boolean,
description: Option[String],
defaultBranch: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastActivityDate: java.util.Date,
originUserName: Option[String],
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String]
)
} }
case class Repository(
userName: String,
repositoryName: String,
isPrivate: Boolean,
description: Option[String],
defaultBranch: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastActivityDate: java.util.Date,
originUserName: Option[String],
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String]
)

View File

@@ -1,22 +1,24 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait SshKeyComponent { self: Profile =>
import profile.simple._
object SshKeys extends Table[SshKey]("SSH_KEY") { lazy val SshKeys = TableQuery[SshKeys]
def userName = column[String]("USER_NAME")
def sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc)
def title = column[String]("TITLE")
def publicKey = column[String]("PUBLIC_KEY")
def ins = userName ~ title ~ publicKey returning sshKeyId class SshKeys(tag: Tag) extends Table[SshKey](tag, "SSH_KEY") {
def * = userName ~ sshKeyId ~ title ~ publicKey <> (SshKey, SshKey.unapply _) val userName = column[String]("USER_NAME")
val sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc)
val title = column[String]("TITLE")
val publicKey = column[String]("PUBLIC_KEY")
def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply)
def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName is userName.bind) && (this.sshKeyId is sshKeyId.bind) def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName is userName.bind) && (this.sshKeyId is sshKeyId.bind)
}
case class SshKey(
userName: String,
sshKeyId: Int = 0,
title: String,
publicKey: String
)
} }
case class SshKey(
userName: String,
sshKeyId: Int,
title: String,
publicKey: String
)

View File

@@ -1,16 +1,20 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait WebHookComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate { lazy val WebHooks = TableQuery[WebHooks]
def url = column[String]("URL")
def * = userName ~ repositoryName ~ url <> (WebHook, WebHook.unapply _)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind) class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
}
case class WebHook(
userName: String,
repositoryName: String,
url: String
)
} }
case class WebHook(
userName: String,
repositoryName: String,
url: String
)

View File

@@ -1,20 +1,24 @@
package object model { package object model extends {
import scala.slick.driver.BasicDriver.Implicit._ // TODO [Slick 2.0]Should be configurable?
import scala.slick.lifted.{Column, MappedTypeMapper} val profile = slick.driver.H2Driver
// TODO [Slick 2.0]To avoid compilation error about delete invocation. Why can't this error be resolved by import profile.simple._?
// java.util.Date TypeMapper val simple = profile.simple
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime), } with AccountComponent
t => new java.util.Date(t.getTime) with ActivityComponent
) with CollaboratorComponent
with GroupMemberComponent
implicit class RichColumn(c1: Column[Boolean]){ with IssueComponent
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1 with IssueCommentComponent
} with IssueLabelComponent
with LabelComponent
/** with MilestoneComponent
* Returns system date. with PullRequestComponent
*/ with RepositoryComponent
def currentDate = new java.util.Date() with SshKeyComponent
with WebHookComponent with Profile {
} /**
* Returns system date.
*/
def currentDate = new java.util.Date()
}

View File

@@ -0,0 +1,88 @@
package plugin
import org.mozilla.javascript.{Context => JsContext}
import org.mozilla.javascript.{Function => JsFunction}
import scala.collection.mutable.ListBuffer
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
class JavaScriptPlugin(val id: String, val version: String,
val author: String, val url: String, val description: String) extends Plugin {
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[Action]()
private val globalActionList = ListBuffer[Action]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[Action] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def addRepositoryMenu(label: String, name: String, url: String, icon: String, condition: JsFunction): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, (context) => {
val context = JsContext.enter()
try {
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
} finally {
JsContext.exit()
}
})
}
def addGlobalMenu(label: String, url: String, icon: String, condition: JsFunction): Unit = {
globalMenuList += GlobalMenu(label, url, icon, (context) => {
val context = JsContext.enter()
try {
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
} finally {
JsContext.exit()
}
})
}
def addGlobalAction(path: String, function: JsFunction): Unit = {
globalActionList += Action(path, (request, response) => {
val context = JsContext.enter()
try {
function.call(context, function, function, Array(request, response))
} finally {
JsContext.exit()
}
})
}
def addRepositoryAction(path: String, function: JsFunction): Unit = {
repositoryActionList += Action(path, (request, response) => {
val context = JsContext.enter()
try {
function.call(context, function, function, Array(request, response))
} finally {
JsContext.exit()
}
})
}
}
object JavaScriptPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new JavaScriptPlugin(id, version, author, url, description)
def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = {
val context = JsContext.enter()
try {
val scope = context.initStandardObjects()
scope.put("PluginSystem", scope, PluginSystem)
scope.put("JavaScriptPlugin", scope, this)
vars.foreach { case (key, value) =>
scope.put(key, scope, value)
}
val result = context.evaluateString(scope, script, "<cmd>", 1, null)
result
} finally {
JsContext.exit
}
}
}

View File

@@ -0,0 +1,16 @@
package plugin
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
trait Plugin {
val id: String
val version: String
val author: String
val url: String
val description: String
def repositoryMenus : List[RepositoryMenu]
def globalMenus : List[GlobalMenu]
def repositoryActions : List[Action]
def globalActions : List[Action]
}

View File

@@ -0,0 +1,123 @@
package plugin
import app.Context
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
import util.Directory._
import util.ControlUtil._
import org.apache.commons.io.FileUtils
import util.JGitUtil
import org.eclipse.jgit.api.Git
/**
* Provides extension points to plug-ins.
*/
object PluginSystem {
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
private val initialized = new AtomicBoolean(false)
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
def install(plugin: Plugin): Unit = {
pluginsMap.put(plugin.id, plugin)
}
def plugins: List[Plugin] = pluginsMap.values.toList
def uninstall(id: String): Unit = {
pluginsMap.remove(id)
}
def repositories: List[PluginRepository] = repositoriesList.toList
/**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/
def init(): Unit = {
if(initialized.compareAndSet(false, true)){
// Load installed plugins
val pluginDir = new java.io.File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
installPlugin(dir.getName)
}
}
// Add default plugin repositories
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
}
}
// TODO Method name seems to not so good.
def installPlugin(id: String): Unit = {
val pluginDir = new java.io.File(PluginHome)
val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js")
if(javaScriptFile.exists && javaScriptFile.isFile){
val properties = new java.util.Properties()
using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in =>
properties.load(in)
}
val script = FileUtils.readFileToString(javaScriptFile, "UTF-8")
try {
JavaScriptPlugin.evaluateJavaScript(script, Map(
"id" -> properties.getProperty("id"),
"version" -> properties.getProperty("version"),
"author" -> properties.getProperty("author"),
"url" -> properties.getProperty("url"),
"description" -> properties.getProperty("description")
))
} catch {
case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e)
}
}
}
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[Action] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
// Case classes to hold plug-ins information internally in GitBucket
case class PluginRepository(id: String, url: String)
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any)
/**
* Checks whether the plugin is updatable.
*/
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
if(oldVersion == newVersion){
false
} else {
val dim1 = oldVersion.split("\\.").map(_.toInt)
val dim2 = newVersion.split("\\.").map(_.toInt)
dim1.zip(dim2).foreach { case (a, b) =>
if(a < b){
return true
} else if(a > b){
return false
}
}
return false
}
}
// TODO This is a test
// addGlobalMenu("Google", "http://www.google.co.jp/", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => context.loginAccount.isDefined }
//
// addRepositoryMenu("Board", "board", "/board", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => true}
//
// addGlobalAction("/hello"){ (request, response) =>
// "Hello World!"
// }
}

View File

@@ -0,0 +1,67 @@
package plugin
import util.Directory._
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
import org.quartz.{Scheduler, JobExecutionContext, Job}
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
class PluginUpdateJob extends Job {
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
private var failedCount = 0
/**
* Clone or pull all plugin repositories
*
* TODO Support plugin repository access through the proxy server
*/
override def execute(context: JobExecutionContext): Unit = {
try {
if(failedCount > 3){
logger.error("Skip plugin information updating because failed count is over limit")
} else {
logger.info("Start plugin information updating")
PluginSystem.repositories.foreach { repository =>
logger.info(s"Updating ${repository.id}: ${repository.url}...")
val dir = getPluginCacheDir()
val repo = new java.io.File(dir, repository.id)
if(repo.exists){
// pull if the repository is already cloned
Git.open(repo).pull().call()
} else {
// clone if the repository is not exist
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
}
}
logger.info("End plugin information updating")
}
} catch {
case e: Exception => {
failedCount = failedCount + 1
logger.error("Failed to update plugin information", e)
}
}
}
}
object PluginUpdateJob {
def schedule(scheduler: Scheduler): Unit = {
// TODO Enable commented code to enable plug-in system
// val job = newJob(classOf[PluginUpdateJob])
// .withIdentity("pluginUpdateJob")
// .build()
//
// val trigger = newTrigger()
// .withIdentity("pluginUpdateTrigger")
// .startNow()
// .withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
// .build()
//
// scheduler.scheduleJob(job, trigger)
}
}

View File

@@ -0,0 +1,38 @@
package plugin
import app.Context
import scala.collection.mutable.ListBuffer
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
// TODO This is a sample implementation for Scala based plug-ins.
class ScalaPlugin(val id: String, val version: String,
val author: String, val url: String, val description: String) extends Plugin {
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[Action]()
private val globalActionList = ListBuffer[Action]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[Action] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
}
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
globalMenuList += GlobalMenu(label, url, icon, condition)
}
def addGlobalAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
globalActionList += Action(path, function)
}
def addRepositoryAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
repositoryActionList += Action(path, function)
}
}

View File

@@ -1,13 +1,11 @@
package service package service
import model._ import model._
import scala.slick.driver.H2Driver.simple._ import simple._
import Database.threadLocalSession // TODO [Slick 2.0]NOT import directly?
import model.dateColumnType
import service.SystemSettingsService.SystemSettings import service.SystemSettingsService.SystemSettings
import util.StringUtil._ import util.StringUtil._
import model.GroupMember
import scala.Some
import model.Account
import util.LDAPUtil import util.LDAPUtil
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -15,7 +13,7 @@ trait AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountService]) private val logger = LoggerFactory.getLogger(classOf[AccountService])
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] = def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] =
if(settings.ldapAuthentication){ if(settings.ldapAuthentication){
ldapAuthentication(settings, userName, password) ldapAuthentication(settings, userName, password)
} else { } else {
@@ -25,7 +23,7 @@ trait AccountService {
/** /**
* Authenticate by internal database. * Authenticate by internal database.
*/ */
private def defaultAuthentication(userName: String, password: String) = { private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = {
getAccountByUserName(userName).collect { getAccountByUserName(userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account) case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
} getOrElse None } getOrElse None
@@ -34,19 +32,35 @@ trait AccountService {
/** /**
* Authenticate by LDAP. * Authenticate by LDAP.
*/ */
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = { private def ldapAuthentication(settings: SystemSettings, userName: String, password: String)
(implicit s: Session): Option[Account] = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match { LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(ldapUserInfo) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(userName, true) match { getAccountByUserName(ldapUserInfo.userName, true) match {
case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) case Some(x) if(!x.isRemoved) => {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if(x.isRemoved) => { case Some(x) if(x.isRemoved) => {
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..") logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password) defaultAuthentication(userName, password)
} }
case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None) case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
case Some(x) if(!x.isRemoved) => {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if(x.isRemoved) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
}
case None => {
createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
getAccountByUserName(ldapUserInfo.userName)
}
}
} }
getAccountByUserName(userName)
} }
case Left(errorMessage) => { case Left(errorMessage) => {
logger.info(s"LDAP Authentication Failed: ${errorMessage}") logger.info(s"LDAP Authentication Failed: ${errorMessage}")
@@ -55,20 +69,21 @@ trait AccountService {
} }
} }
def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] = def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption Accounts filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] = def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption Accounts filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true): List[Account] = def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] =
if(includeRemoved){ if(includeRemoved){
Query(Accounts) sortBy(_.userName) list Accounts sortBy(_.userName) list
} else { } else {
Query(Accounts) filter (_.removed is false.bind) sortBy(_.userName) list Accounts filter (_.removed is false.bind) sortBy(_.userName) list
} }
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit = def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
(implicit s: Session): Unit =
Accounts insert Account( Accounts insert Account(
userName = userName, userName = userName,
password = password, password = password,
@@ -83,14 +98,14 @@ trait AccountService {
isGroupAccount = false, isGroupAccount = false,
isRemoved = false) isRemoved = false)
def updateAccount(account: Account): Unit = def updateAccount(account: Account)(implicit s: Session): Unit =
Accounts Accounts
.filter { a => a.userName is account.userName.bind } .filter { a => a.userName is account.userName.bind }
.map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? ~ a.removed } .map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) }
.update ( .update (
account.password, account.password,
account.fullName, account.fullName,
account.mailAddress, account.mailAddress,
account.isAdmin, account.isAdmin,
account.url, account.url,
account.registeredDate, account.registeredDate,
@@ -98,13 +113,13 @@ trait AccountService {
account.lastLoginDate, account.lastLoginDate,
account.isRemoved) account.isRemoved)
def updateAvatarImage(userName: String, image: Option[String]): Unit = def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit =
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image) Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
def updateLastLoginDate(userName: String): Unit = def updateLastLoginDate(userName: String)(implicit s: Session): 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 = def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit =
Accounts insert Account( Accounts insert Account(
userName = groupName, userName = groupName,
password = "", password = "",
@@ -119,35 +134,35 @@ trait AccountService {
isGroupAccount = true, isGroupAccount = true,
isRemoved = false) isRemoved = false)
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit = def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit =
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed) Accounts.filter(_.userName is groupName.bind).map(t => t.url.? -> t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = { def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete GroupMembers.filter(_.groupName is groupName.bind).delete
members.foreach { case (userName, isManager) => members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager) GroupMembers insert GroupMember (groupName, userName, isManager)
} }
} }
def getGroupMembers(groupName: String): List[GroupMember] = def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] =
Query(GroupMembers) GroupMembers
.filter(_.groupName is groupName.bind) .filter(_.groupName is groupName.bind)
.sortBy(_.userName) .sortBy(_.userName)
.list .list
def getGroupsByUserName(userName: String): List[String] = def getGroupsByUserName(userName: String)(implicit s: Session): List[String] =
Query(GroupMembers) GroupMembers
.filter(_.userName is userName.bind) .filter(_.userName is userName.bind)
.sortBy(_.groupName) .sortBy(_.groupName)
.map(_.groupName) .map(_.groupName)
.list .list
def removeUserRelatedData(userName: String): Unit = { def removeUserRelatedData(userName: String)(implicit s: Session): Unit = {
Query(GroupMembers).filter(_.userName is userName.bind).delete GroupMembers.filter(_.userName is userName.bind).delete
Query(Collaborators).filter(_.collaboratorName is userName.bind).delete Collaborators.filter(_.collaboratorName is userName.bind).delete
Query(Repositories).filter(_.userName is userName.bind).delete Repositories.filter(_.userName is userName.bind).delete
} }
} }
object AccountService extends AccountService object AccountService extends AccountService

View File

@@ -1,12 +1,11 @@
package service package service
import model._ import model._
import scala.slick.driver.H2Driver.simple._ import simple._
import Database.threadLocalSession
trait ActivityService { trait ActivityService {
def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
Activities Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => .filter { case (t1, t2) =>
@@ -21,7 +20,7 @@ trait ActivityService {
.take(30) .take(30)
.list .list
def getRecentActivities(): List[Activity] = def getRecentActivities()(implicit s: Session): List[Activity] =
Activities Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => t2.isPrivate is false.bind } .filter { case (t1, t2) => t2.isPrivate is false.bind }
@@ -29,143 +28,142 @@ trait ActivityService {
.map { case (t1, t2) => t1 } .map { case (t1, t2) => t1 }
.take(30) .take(30)
.list .list
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit = def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"create_repository", "create_repository",
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit = def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"open_issue", "open_issue",
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit = def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"close_issue", "close_issue",
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit = def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"close_issue", "close_issue",
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit = def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"reopen_issue", "reopen_issue",
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit = def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_issue", "comment_issue",
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)),
currentDate) currentDate)
def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit = def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_issue", "comment_issue",
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)),
currentDate) currentDate)
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) = def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"create_wiki", "create_wiki",
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki", s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
Some(pageName), Some(pageName),
currentDate) currentDate)
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) = def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"edit_wiki", "edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
Some(pageName + ":" + commitId), Some(pageName + ":" + commitId),
currentDate) currentDate)
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String, def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
branchName: String, commits: List[util.JGitUtil.CommitInfo]) = branchName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities insert Activity(userName, repositoryName, activityUserName,
"push", "push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")), Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
currentDate) currentDate)
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String, def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo]) = tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities insert Activity(userName, repositoryName, activityUserName,
"create_tag", "create_tag",
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String, def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo]) = tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities insert Activity(userName, repositoryName, activityUserName,
"delete_tag", "delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) = def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"create_branch", "create_branch",
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) = def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"delete_branch", "delete_branch",
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) = def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities insert Activity(userName, repositoryName, activityUserName,
"fork", "fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]", s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit = def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"open_pullreq", "open_pullreq",
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title),
currentDate) currentDate)
def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit = def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String)
Activities.autoInc insert(userName, repositoryName, activityUserName, (implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"merge_pullreq", "merge_pullreq",
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(message), Some(message),
currentDate) currentDate)
def insertCommitId(userName: String, repositoryName: String, commitId: String) = { private def cut(value: String, length: Int): String =
CommitLog insert (userName, repositoryName, commitId)
}
def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) =
CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*)
def getAllCommitIds(userName: String, repositoryName: String): List[String] =
Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list
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 if(value.length > length) value.substring(0, length) + "..." else value
} }

View File

@@ -1,380 +1,386 @@
package service package service
import scala.slick.driver.H2Driver.simple._ import scala.slick.jdbc.{StaticQuery => Q}
import Database.threadLocalSession import Q.interpolation
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation import model._
import simple._
import model._ import util.Implicits._
import util.Implicits._ import util.StringUtil._
import util.StringUtil._
trait IssuesService {
trait IssuesService { import IssuesService._
import IssuesService._
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
def getIssue(owner: String, repository: String, issueId: String) = if (issueId forall (_.isDigit))
if (issueId forall (_.isDigit)) Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption else None
else None
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
def getComments(owner: String, repository: String, issueId: Int) = IssueComments filter (_.byIssue(owner, repository, issueId)) list
Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
def getComment(owner: String, repository: String, commentId: String) = if (commentId forall (_.isDigit))
if (commentId forall (_.isDigit)) IssueComments filter { t =>
Query(IssueComments) filter { t => t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) } firstOption
} firstOption else None
else None
def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) =
def getIssueLabels(owner: String, repository: String, issueId: Int) = IssueLabels
IssueLabels .innerJoin(Labels).on { (t1, t2) =>
.innerJoin(Labels).on { (t1, t2) => t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) }
} .filter ( _._1.byIssue(owner, repository, issueId) )
.filter ( _._1.byIssue(owner, repository, issueId) ) .map ( _._2 )
.map ( _._2 ) .list
.list
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
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 condition the search condition
* @param condition the search condition * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. * @param repos Tuple of the repository owner and the repository name
* @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(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, repos: (String, String)*)(implicit s: Session): Int =
repos: (String, String)*): Int = // TODO check SQL
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
/**
* Returns the Map which contains issue count for each labels. /**
* * Returns the Map which contains issue count for each labels.
* @param owner the repository owner *
* @param repository the repository name * @param owner the repository owner
* @param condition the search condition * @param repository the repository name
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) * @param condition the search condition
* @return the Map which contains issue count for each labels (key is label name, value is issue count) * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
*/ * @return the Map which contains issue count for each labels (key is label name, value is issue count)
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, */
filterUser: Map[String, String]): Map[String, Int] = { def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
.innerJoin(IssueLabels).on { (t1, t2) => searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) .innerJoin(IssueLabels).on { (t1, t2) =>
} t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
.innerJoin(Labels).on { case ((t1, t2), t3) => }
t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) .innerJoin(Labels).on { case ((t1, t2), t3) =>
} t2.byLabel(t3.userName, t3.repositoryName, t3.labelId)
.groupBy { case ((t1, t2), t3) => }
t3.labelName .groupBy { case ((t1, t2), t3) =>
} t3.labelName
.map { case (labelName, t) => }
labelName ~ t.length .map { case (labelName, t) =>
} labelName -> t.length
.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. * 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 condition the search condition
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. * @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 * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @return list which contains issue count for each repository * @param repos Tuple of the repository owner and the repository name
*/ * @return list which contains issue count for each repository
def countIssueGroupByRepository( */
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, def countIssueGroupByRepository(
repos: (String, String)*): List[(String, String, Int)] = { condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
.groupBy { t => searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
t.userName ~ t.repositoryName .groupBy { t =>
} t.userName -> t.repositoryName
.map { case (repo, t) => }
repo ~ t.length .map { case (repo, t) =>
} (repo._1, repo._2, t.length)
.sortBy(_._3 desc) }
.list .sortBy(_._3 desc)
} .list
}
/**
* Returns the search result against issues. /**
* * Returns the search result against issues.
* @param condition the search condition *
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) * @param condition the search condition
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
* @param offset the offset for pagination * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param limit the limit for pagination * @param offset the offset for pagination
* @param repos Tuple of the repository owner and the repository name * @param limit the limit for pagination
* @return the search result (list of tuples which contain issue, labels and comment count) * @param repos Tuple of the repository owner and the repository name
*/ * @return the search result (list of tuples which contain issue, labels and comment count)
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, */
offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = { def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
offset: Int, limit: Int, repos: (String, String)*)
// get issues and comment count and labels (implicit s: Session): List[(Issue, List[Label], Int)] = {
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } // get issues and comment count and labels
.sortBy { case (t1, t2) => searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
(condition.sort match { .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
case "created" => t1.registeredDate .sortBy { case (t1, t2) =>
case "comments" => t2.commentCount (condition.sort match {
case "updated" => t1.updatedDate case "created" => t1.registeredDate
}) match { case "comments" => t2.commentCount
case sort => condition.direction match { case "updated" => t1.updatedDate
case "asc" => sort asc }) match {
case "desc" => sort desc case sort => condition.direction match {
} case "asc" => sort asc
} case "desc" => sort desc
} }
.drop(offset).take(limit) }
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } .drop(offset).take(limit)
.map { case (((t1, t2), t3), t4) => .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
} .map { case (((t1, t2), t3), t4) =>
.list (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
.splitWith { (c1, c2) => }
c1._1.userName == c2._1.userName && .list
c1._1.repositoryName == c2._1.repositoryName && .splitWith { (c1, c2) =>
c1._1.issueId == c2._1.issueId c1._1.userName == c2._1.userName &&
} c1._1.repositoryName == c2._1.repositoryName &&
.map { issues => issues.head match { c1._1.issueId == c2._1.issueId
case (issue, commentCount, _,_,_) => }
(issue, .map { issues => issues.head match {
issues.flatMap { t => t._3.map ( case (issue, commentCount, _,_,_) =>
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) (issue,
)} toList, issues.flatMap { t => t._3.map (
commentCount) Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
}} toList )} toList,
} commentCount)
}} toList
/** }
* Assembles query for conditional issue searching.
*/ /**
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, * Assembles query for conditional issue searching.
filterUser: Map[String, String], onlyPullRequest: Boolean) = */
Query(Issues) filter { t1 => private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
condition.repo filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } Issues filter { t1 =>
.getOrElse (repos) condition.repo
.map { case (owner, repository) => t1.byRepository(owner, repository) } .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.foldLeft[Column[Boolean]](false) ( _ || _ ) && .getOrElse (repos)
(t1.closed is (condition.state == "closed").bind) && .map { case (owner, repository) => t1.byRepository(owner, repository) }
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) && (t1.closed is (condition.state == "closed").bind) &&
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && (t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.pullRequest is true.bind, onlyPullRequest) && (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(IssueLabels filter { t2 => (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t1.pullRequest is true.bind, onlyPullRequest) &&
(t2.labelId in (IssueLabels filter { t2 =>
(Labels filter { t3 => (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t3.byRepository(t1.userName, t1.repositoryName)) && (t2.labelId in
(t3.labelName inSetBind condition.labels) (Labels filter { t3 =>
} map(_.labelId))) (t3.byRepository(t1.userName, t1.repositoryName)) &&
} exists, condition.labels.nonEmpty) (t3.labelName inSetBind condition.labels)
} } map(_.labelId)))
} exists, condition.labels.nonEmpty)
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], }
assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
// next id number def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] assignedUserName: Option[String], milestoneId: Option[Int],
.firstOption.filter { id => isPullRequest: Boolean = false)(implicit s: Session) =
Issues insert Issue( // next id number
owner, sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
repository, .firstOption.filter { id =>
id, Issues insert Issue(
loginUser, owner,
milestoneId, repository,
assignedUserName, id,
title, loginUser,
content, milestoneId,
false, assignedUserName,
currentDate, title,
currentDate, content,
isPullRequest) false,
currentDate,
// increment issue id currentDate,
IssueId isPullRequest)
.filter (_.byPrimaryKey(owner, repository))
.map (_.issueId) // increment issue id
.update (id) > 0 IssueId
} get .filter (_.byPrimaryKey(owner, repository))
.map (_.issueId)
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = .update (id) > 0
IssueLabels insert (IssueLabel(owner, repository, issueId, labelId)) } get
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete IssueLabels insert IssueLabel(owner, repository, issueId, labelId)
def createComment(owner: String, repository: String, loginUser: String, def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
issueId: Int, content: String, action: String) = IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
IssueComments.autoInc insert (
owner, def createComment(owner: String, repository: String, loginUser: String,
repository, issueId: Int, content: String, action: String)(implicit s: Session): Int =
issueId, IssueComments.autoInc insert IssueComment(
action, userName = owner,
loginUser, repositoryName = repository,
content, issueId = issueId,
currentDate, action = action,
currentDate) commentedUserName = loginUser,
content = content,
def updateIssue(owner: String, repository: String, issueId: Int, registeredDate = currentDate,
title: String, content: Option[String]) = updatedDate = currentDate)
Issues
.filter (_.byPrimaryKey(owner, repository, issueId)) def updateIssue(owner: String, repository: String, issueId: Int,
.map { t => title: String, content: Option[String])(implicit s: Session) =
t.title ~ t.content.? ~ t.updatedDate Issues
} .filter (_.byPrimaryKey(owner, repository, issueId))
.update (title, content, currentDate) .map { t =>
(t.title, t.content.?, t.updatedDate)
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) = }
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) .update (title, content, currentDate)
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) = def updateAssignedUserName(owner: String, repository: String, issueId: Int,
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) assignedUserName: Option[String])(implicit s: Session) =
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
def updateComment(commentId: Int, content: String) =
IssueComments def updateMilestoneId(owner: String, repository: String, issueId: Int,
.filter (_.byPrimaryKey(commentId)) milestoneId: Option[Int])(implicit s: Session) =
.map { t => Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
t.content ~ t.updatedDate
} def updateComment(commentId: Int, content: String)(implicit s: Session) =
.update (content, currentDate) IssueComments
.filter (_.byPrimaryKey(commentId))
def deleteComment(commentId: Int) = .map { t =>
IssueComments filter (_.byPrimaryKey(commentId)) delete t.content -> t.updatedDate
}
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) = .update (content, currentDate)
Issues
.filter (_.byPrimaryKey(owner, repository, issueId)) def deleteComment(commentId: Int)(implicit s: Session) =
.map { t => IssueComments filter (_.byPrimaryKey(commentId)) delete
t.closed ~ t.updatedDate
} def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) =
.update (closed, currentDate) Issues
.filter (_.byPrimaryKey(owner, repository, issueId))
/** .map { t =>
* Search issues by keyword. t.closed -> t.updatedDate
* }
* @param owner the repository owner .update (closed, currentDate)
* @param repository the repository name
* @param query the keywords separated by whitespace. /**
* @return issues with comment count and matched content of issue or comment * Search issues by keyword.
*/ *
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = { * @param owner the repository owner
import scala.slick.driver.H2Driver.likeEncode * @param repository the repository name
val keywords = splitWords(query.toLowerCase) * @param query the keywords separated by whitespace.
* @return issues with comment count and matched content of issue or comment
// Search Issue */
val issues = Issues def searchIssuesByKeyword(owner: String, repository: String, query: String)
.innerJoin(IssueOutline).on { case (t1, t2) => (implicit s: Session): List[(Issue, Int, String)] = {
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) import slick.driver.JdbcDriver.likeEncode
} val keywords = splitWords(query.toLowerCase)
.filter { case (t1, t2) =>
keywords.map { keyword => // Search Issue
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || val issues = Issues
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) .innerJoin(IssueOutline).on { case (t1, t2) =>
} .reduceLeft(_ && _) t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
} }
.map { case (t1, t2) => .filter { case (t1, t2) =>
(t1, 0, t1.content.?, t2.commentCount) keywords.map { keyword =>
} (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
// Search IssueComment } .reduceLeft(_ && _)
val comments = IssueComments }
.innerJoin(Issues).on { case (t1, t2) => .map { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) (t1, 0, t1.content.?, t2.commentCount)
} }
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) // Search IssueComment
} val comments = IssueComments
.filter { case ((t1, t2), t3) => .innerJoin(Issues).on { case (t1, t2) =>
keywords.map { query => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') }
}.reduceLeft(_ && _) .innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
} t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
.map { case ((t1, t2), t3) => }
(t2, t1.commentId, t1.content.?, t3.commentCount) .filter { case ((t1, t2), t3) =>
} keywords.map { query =>
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
issues.union(comments).sortBy { case (issue, commentId, _, _) => }.reduceLeft(_ && _)
issue.issueId ~ commentId }
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => .map { case ((t1, t2), t3) =>
issue1.issueId == issue2.issueId (t2, t1.commentId, t1.content.?, t3.commentCount)
}.map { _.head match { }
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
} issues.union(comments).sortBy { case (issue, commentId, _, _) =>
}.toList issue.issueId -> commentId
} }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issue1.issueId == issue2.issueId
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = { }.map { _.head match {
extractCloseId(message).foreach { issueId => case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ }
createComment(owner, repository, userName, issue.issueId, "Close", "close") }.toList
updateClosed(owner, repository, issue.issueId, true) }
}
} def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = {
} extractCloseId(message).foreach { issueId =>
} for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
createComment(owner, repository, userName, issue.issueId, "Close", "close")
object IssuesService { updateClosed(owner, repository, issue.issueId, true)
import javax.servlet.http.HttpServletRequest }
}
val IssueLimit = 30 }
}
case class IssueSearchCondition(
labels: Set[String] = Set.empty, object IssuesService {
milestoneId: Option[Option[Int]] = None, import javax.servlet.http.HttpServletRequest
repo: Option[String] = None,
state: String = "open", val IssueLimit = 30
sort: String = "created",
direction: String = "desc"){ case class IssueSearchCondition(
labels: Set[String] = Set.empty,
def toURL: String = milestoneId: Option[Option[Int]] = None,
"?" + List( repo: Option[String] = None,
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), state: String = "open",
milestoneId.map { id => "milestone=" + (id match { sort: String = "created",
case Some(x) => x.toString direction: String = "desc"){
case None => "none"
})}, def toURL: String =
repo.map("for=" + urlEncode(_)), "?" + List(
Some("state=" + urlEncode(state)), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
Some("sort=" + urlEncode(sort)), milestoneId.map { id => "milestone=" + (id match {
Some("direction=" + urlEncode(direction))).flatten.mkString("&") case Some(x) => x.toString
case None => "none"
} })},
repo.map("for=" + urlEncode(_)),
object IssueSearchCondition { Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { Some("direction=" + urlEncode(direction))).flatten.mkString("&")
val value = request.getParameter(name)
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) }
}
object IssueSearchCondition {
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition( private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), val value = request.getParameter(name)
param(request, "milestone").map{ if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
case "none" => None }
case x => x.toIntOpt
}, def apply(request: HttpServletRequest): IssueSearchCondition =
param(request, "for"), IssueSearchCondition(
param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), param(request, "milestone").map{
param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) case "none" => None
case x => x.toIntOpt
def page(request: HttpServletRequest) = try { },
val i = param(request, "page").getOrElse("1").toInt param(request, "for"),
if(i <= 0) 1 else i param(request, "state", Seq("open", "closed")).getOrElse("open"),
} catch { param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
case e: NumberFormatException => 1 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

@@ -1,26 +1,31 @@
package service package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._ import model._
import simple._
trait LabelsService { trait LabelsService {
def getLabels(owner: String, repository: String): List[Label] = def getLabels(owner: String, repository: String)(implicit s: Session): List[Label] =
Query(Labels).filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list Labels.filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list
def getLabel(owner: String, repository: String, labelId: Int): Option[Label] = def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
Query(Labels).filter(_.byPrimaryKey(owner, repository, labelId)).firstOption Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String): Unit = def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit =
Labels.ins insert (owner, repository, labelName, color) Labels insert Label(
userName = owner,
repositoryName = repository,
labelName = labelName,
color = color
)
def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String): Unit = def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String)
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).map(t => t.labelName ~ t.color) (implicit s: Session): Unit =
.update(labelName, color) Labels.filter(_.byPrimaryKey(owner, repository, labelId))
.map(t => t.labelName -> t.color)
.update(labelName, color)
def deleteLabel(owner: String, repository: String, labelId: Int): Unit = { def deleteLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Unit = {
IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete
} }

View File

@@ -1,39 +1,48 @@
package service package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._ import model._
import simple._
// TODO [Slick 2.0]NOT import directly?
import model.dateColumnType
trait MilestonesService { trait MilestonesService {
def createMilestone(owner: String, repository: String, title: String, description: Option[String], def createMilestone(owner: String, repository: String, title: String, description: Option[String],
dueDate: Option[java.util.Date]): Unit = dueDate: Option[java.util.Date])(implicit s: Session): Unit =
Milestones.ins insert (owner, repository, title, description, dueDate, None) Milestones insert Milestone(
userName = owner,
repositoryName = repository,
title = title,
description = description,
dueDate = dueDate,
closedDate = None
)
def updateMilestone(milestone: Milestone): Unit = def updateMilestone(milestone: Milestone)(implicit s: Session): Unit =
Milestones Milestones
.filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId)) .filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId))
.map (t => t.title ~ t.description.? ~ t.dueDate.? ~ t.closedDate.?) .map (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?))
.update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate) .update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate)
def openMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = None)) def openMilestone(milestone: Milestone)(implicit s: Session): Unit =
updateMilestone(milestone.copy(closedDate = None))
def closeMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = Some(currentDate))) def closeMilestone(milestone: Milestone)(implicit s: Session): Unit =
updateMilestone(milestone.copy(closedDate = Some(currentDate)))
def deleteMilestone(owner: String, repository: String, milestoneId: Int): Unit = { def deleteMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Unit = {
Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None) Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None)
Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete
} }
def getMilestone(owner: String, repository: String, milestoneId: Int): Option[Milestone] = def getMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Option[Milestone] =
Query(Milestones).filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption
def getMilestonesWithIssueCount(owner: String, repository: String): List[(Milestone, Int, Int)] = { def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
val counts = Issues val counts = Issues
.filter { t => (t.byRepository(owner, repository)) && (t.milestoneId isNotNull) } .filter { t => (t.byRepository(owner, repository)) && (t.milestoneId isNotNull) }
.groupBy { t => t.milestoneId ~ t.closed } .groupBy { t => t.milestoneId -> t.closed }
.map { case (t1, t2) => (t1._1 ~ t1._2) -> t2.length } .map { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
.toMap .toMap
getMilestones(owner, repository).map { milestone => getMilestones(owner, repository).map { milestone =>
@@ -41,6 +50,7 @@ trait MilestonesService {
} }
} }
def getMilestones(owner: String, repository: String): List[Milestone] = def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] =
Query(Milestones).filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
} }

View File

@@ -1,41 +1,44 @@
package service package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._ import model._
import util.ControlUtil._ import simple._
trait PullRequestService { self: IssuesService => trait PullRequestService { self: IssuesService =>
import PullRequestService._ import PullRequestService._
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] = def getPullRequest(owner: String, repository: String, issueId: Int)
(implicit s: Session): Option[(Issue, PullRequest)] =
getIssue(owner, repository, issueId.toString).flatMap{ issue => getIssue(owner, repository, issueId.toString).flatMap{ issue =>
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{ PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
pullreq => (issue, pullreq) pullreq => (issue, pullreq)
} }
} }
def updateCommitIdTo(owner: String, repository: String, issueId: Int, commitIdTo: String): Unit = def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String)
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdTo).update(commitIdTo) (implicit s: Session): Unit =
PullRequests.filter(_.byPrimaryKey(owner, repository, issueId))
.map(pr => pr.commitIdTo -> pr.commitIdFrom)
.update((commitIdTo, commitIdFrom))
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] = def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])
Query(PullRequests) (implicit s: Session): List[PullRequestCount] =
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) => .filter { case (t1, t2) =>
(t2.closed is closed.bind) && (t2.closed is closed.bind) &&
(t1.userName is owner.bind) && (t1.userName is owner.get.bind, owner.isDefined) &&
(t1.repositoryName is repository.get.bind, repository.isDefined) (t1.repositoryName is repository.get.bind, repository.isDefined)
} }
.groupBy { case (t1, t2) => t2.openedUserName } .groupBy { case (t1, t2) => t2.openedUserName }
.map { case (userName, t) => userName ~ t.length } .map { case (userName, t) => userName -> t.length }
.sortBy(_._2 desc) .sortBy(_._2 desc)
.list .list
.map { x => PullRequestCount(x._1, x._2) } .map { x => PullRequestCount(x._1, x._2) }
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
commitIdFrom: String, commitIdTo: String): Unit = commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit =
PullRequests insert (PullRequest( PullRequests insert PullRequest(
originUserName, originUserName,
originRepositoryName, originRepositoryName,
issueId, issueId,
@@ -44,10 +47,11 @@ trait PullRequestService { self: IssuesService =>
requestRepositoryName, requestRepositoryName,
requestBranch, requestBranch,
commitIdFrom, commitIdFrom,
commitIdTo)) commitIdTo)
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean): List[PullRequest] = def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean)
Query(PullRequests) (implicit s: Session): List[PullRequest] =
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) => .filter { case (t1, t2) =>
(t1.requestUserName is userName.bind) && (t1.requestUserName is userName.bind) &&

View File

@@ -3,21 +3,20 @@ package service
import util.{FileUtil, StringUtil, JGitUtil} import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import model.Issue
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.treewalk.TreeWalk
import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import model._
import simple._
trait trait RepositorySearchService { self: IssuesService =>
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._ import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int = def countIssues(owner: String, repository: String, query: String)(implicit session: Session): Int =
searchIssuesByKeyword(owner, repository, query).length searchIssuesByKeyword(owner, repository, query).length
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] = def searchIssues(owner: String, repository: String, query: String)(implicit session: Session): List[IssueSearchResult] =
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) => searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult( IssueSearchResult(
issue.issueId, issue.issueId,
@@ -39,7 +38,7 @@ RepositorySearchService { self: IssuesService =>
Nil Nil
} else { } else {
val files = searchRepositoryFiles(git, query) val files = searchRepositoryFiles(git, query)
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD") val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD")
files.map { case (path, text) => files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query) val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult( FileSearchResult(
@@ -60,7 +59,7 @@ RepositorySearchService { self: IssuesService =>
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
val keywords = StringUtil.splitWords(query.toLowerCase) val keywords = StringUtil.splitWords(query.toLowerCase)
val list = new ListBuffer[(String, String)] val list = new scala.collection.mutable.ListBuffer[(String, String)]
while (treeWalk.next()) { while (treeWalk.next()) {
val mode = treeWalk.getFileMode(0) val mode = treeWalk.getFileMode(0)

View File

@@ -1,8 +1,7 @@
package service package service
import model._ import model._
import scala.slick.driver.H2Driver.simple._ import simple._
import Database.threadLocalSession
import util.JGitUtil import util.JGitUtil
trait RepositoryService { self: AccountService => trait RepositoryService { self: AccountService =>
@@ -20,7 +19,8 @@ trait RepositoryService { self: AccountService =>
*/ */
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None, originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = { parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
(implicit s: Session): Unit = {
Repositories insert Repositories insert
Repository( Repository(
userName = userName, userName = userName,
@@ -39,70 +39,74 @@ trait RepositoryService { self: AccountService =>
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = { def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String)
(Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => (implicit s: Session): Unit = {
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) getAccountByUserName(newUserName).foreach { account =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t => Repositories.filter { t =>
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind) (t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t => Repositories.filter { t =>
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind) (t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t => PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind t.requestRepositoryName is oldRepositoryName.bind
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
deleteRepository(oldUserName, oldRepositoryName) deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitLog .insertAll(commitLog .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) if(account.isGroupAccount){
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
}
// Update activity messages // Update activity messages
val updateActivities = Activities.filter { t => val updateActivities = Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId ~ t.message }.list }.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) => updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId is activityId.bind).map(_.message).update( Activities.filter(_.activityId is activityId.bind).map(_.message).update(
message message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#") .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#") .replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#") .replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#") .replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
) )
}
} }
} }
} }
def deleteRepository(userName: String, repositoryName: String): Unit = { def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete Collaborators .filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete Labels .filter(_.byRepository(userName, repositoryName)).delete
@@ -121,8 +125,8 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of repository owner * @param userName the user name of repository owner
* @return the list of repository names * @return the list of repository names
*/ */
def getRepositoryNamesOfUser(userName: String): List[String] = def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list Repositories filter(_.userName is userName.bind) map (_.repositoryName) list
/** /**
* Returns the specified repository information. * Returns the specified repository information.
@@ -132,10 +136,10 @@ trait RepositoryService { self: AccountService =>
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @return the repository information * @return the repository information
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count // for getting issue count and pull request count
val issues = Query(Issues).filter { t => val issues = Issues.filter { t =>
t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind) t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
}.map(_.pullRequest).list }.map(_.pullRequest).list
@@ -152,13 +156,18 @@ trait RepositoryService { self: AccountService =>
} }
} }
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = { def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
Query(Repositories).filter { t1 => (implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName is userName.bind) || (t1.userName is userName.bind) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists) (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
@@ -175,24 +184,32 @@ trait RepositoryService { self: AccountService =>
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application * @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) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @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] = { def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories) case Some(x) if(x.isAdmin) => 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) || (t.userName is x.userName) || Repositories filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists) (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 => Repositories filter(_.isPrivate is false.bind)
}).filter { t => }).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE repositoryUserName.map { userName => t.userName is userName.bind } getOrElse LiteralColumn(true)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
@@ -202,7 +219,7 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getRepositoryManagers(userName: String): Seq[String] = private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){ if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else { } else {
@@ -212,16 +229,16 @@ trait RepositoryService { self: AccountService =>
/** /**
* Updates the last activity date of the repository. * Updates the last activity date of the repository.
*/ */
def updateLastActivityDate(userName: String, repositoryName: String): Unit = def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate) Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
/** /**
* Save repository options. * Save repository options.
*/ */
def saveRepositoryOptions(userName: String, repositoryName: String, def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], defaultBranch: String, isPrivate: Boolean): Unit = description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)) Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => r.description.? ~ r.defaultBranch ~ r.isPrivate ~ r.updatedDate } .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
.update (description, defaultBranch, isPrivate, currentDate) .update (description, defaultBranch, isPrivate, currentDate)
/** /**
@@ -231,8 +248,8 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators insert(Collaborator(userName, repositoryName, collaboratorName)) Collaborators insert Collaborator(userName, repositoryName, collaboratorName)
/** /**
* Remove collaborator from the repository. * Remove collaborator from the repository.
@@ -241,7 +258,7 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/** /**
@@ -250,7 +267,7 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param repositoryName the repository name * @param repositoryName the repository name
*/ */
def removeCollaborators(userName: String, repositoryName: String): Unit = def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/** /**
@@ -260,10 +277,10 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @return the list of collaborators name * @return the list of collaborators name
*/ */
def getCollaborators(userName: String, repositoryName: String): List[String] = def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] =
Query(Collaborators).filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account]): Boolean = { def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match { loginAccount match {
case Some(a) if(a.isAdmin) => true case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true case Some(a) if(a.userName == owner) => true
@@ -272,17 +289,18 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getForkedCount(userName: String, repositoryName: String): Int = private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
// TODO check SQL
Query(Repositories.filter { t => Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.length).first }.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] = def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
Query(Repositories).filter { t => Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
} }
.sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
} }

View File

@@ -1,6 +1,8 @@
package service package service
import model._ import model._
import slick.jdbc.JdbcBackend
import util.Implicits.request2Session
/** /**
* This service is used for a view helper mainly. * This service is used for a view helper mainly.
@@ -10,22 +12,27 @@ import model._
*/ */
trait RequestCache extends SystemSettingsService with AccountService with IssuesService { trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = { private implicit def context2Session(implicit context: app.Context): JdbcBackend#Session =
request2Session(context.request)
def getIssue(userName: String, repositoryName: String, issueId: String)
(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){ context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
super.getIssue(userName, repositoryName, issueId) super.getIssue(userName, repositoryName, issueId)
} }
} }
def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = { def getAccountByUserName(userName: String)
(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${userName}"){ context.cache(s"account.${userName}"){
super.getAccountByUserName(userName) super.getAccountByUserName(userName)
} }
} }
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = { def getAccountByMailAddress(mailAddress: String)
(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){ context.cache(s"account.${mailAddress}"){
super.getAccountByMailAddress(mailAddress) super.getAccountByMailAddress(mailAddress)
} }
} }
} }

View File

@@ -1,19 +1,17 @@
package service package service
import model._ import model._
import scala.slick.driver.H2Driver.simple._ import simple._
import Database.threadLocalSession
trait SshKeyService { trait SshKeyService {
def addPublicKey(userName: String, title: String, publicKey: String): Unit = def addPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit =
SshKeys.ins insert (userName, title, publicKey) SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey)
def getPublicKeys(userName: String): List[SshKey] = def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] =
Query(SshKeys).filter(_.userName is userName.bind).sortBy(_.sshKeyId).list SshKeys.filter(_.userName is userName.bind).sortBy(_.sshKeyId).list
def deletePublicKey(userName: String, sshKeyId: Int): Unit = def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit =
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
} }

View File

@@ -1,184 +1,190 @@
package service package service
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
trait SystemSettingsService { trait SystemSettingsService {
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse { def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request)
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) def saveSystemSettings(settings: SystemSettings): Unit = {
} defining(new java.util.Properties()){ props =>
}.replaceFirst("/$", "") settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
def saveSystemSettings(settings: SystemSettings): Unit = { props.setProperty(Gravatar, settings.gravatar.toString)
defining(new java.util.Properties()){ props => props.setProperty(Notification, settings.notification.toString)
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) props.setProperty(Ssh, settings.ssh.toString)
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
props.setProperty(Gravatar, settings.gravatar.toString) if(settings.notification) {
props.setProperty(Notification, settings.notification.toString) settings.smtp.foreach { smtp =>
props.setProperty(Ssh, settings.ssh.toString) props.setProperty(SmtpHost, smtp.host)
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
if(settings.notification) { smtp.user.foreach(props.setProperty(SmtpUser, _))
settings.smtp.foreach { smtp => smtp.password.foreach(props.setProperty(SmtpPassword, _))
props.setProperty(SmtpHost, smtp.host) smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
smtp.user.foreach(props.setProperty(SmtpUser, _)) smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
smtp.password.foreach(props.setProperty(SmtpPassword, _)) }
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) }
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) if(settings.ldapAuthentication){
} settings.ldap.map { ldap =>
} props.setProperty(LdapHost, ldap.host)
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
if(settings.ldapAuthentication){ ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
settings.ldap.map { ldap => ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
props.setProperty(LdapHost, ldap.host) props.setProperty(LdapBaseDN, ldap.baseDN)
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
props.setProperty(LdapBaseDN, ldap.baseDN) ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) }
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) }
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) using(new java.io.FileOutputStream(GitBucketConf)){ out =>
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) props.store(out, null)
} }
} }
props.store(new java.io.FileOutputStream(GitBucketConf), null) }
}
}
def loadSystemSettings(): SystemSettings = {
defining(new java.util.Properties()){ props =>
def loadSystemSettings(): SystemSettings = { if(GitBucketConf.exists){
defining(new java.util.Properties()){ props => using(new java.io.FileInputStream(GitBucketConf)){ in =>
if(GitBucketConf.exists){ props.load(in)
props.load(new java.io.FileInputStream(GitBucketConf)) }
} }
SystemSettings( SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getValue(props, AllowAccountRegistration, false), getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true), getValue(props, Gravatar, true),
getValue(props, Notification, false), getValue(props, Notification, false),
getValue(props, Ssh, false), getValue(props, Ssh, false),
getOptionValue(props, SshPort, Some(DefaultSshPort)), getOptionValue(props, SshPort, Some(DefaultSshPort)),
if(getValue(props, Notification, false)){ if(getValue(props, Notification, false)){
Some(Smtp( Some(Smtp(
getValue(props, SmtpHost, ""), getValue(props, SmtpHost, ""),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None), getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None), getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None), getOptionValue[Boolean](props, SmtpSsl, None),
getOptionValue(props, SmtpFromAddress, None), getOptionValue(props, SmtpFromAddress, None),
getOptionValue(props, SmtpFromName, None))) getOptionValue(props, SmtpFromName, None)))
} else { } else {
None None
}, },
getValue(props, LdapAuthentication, false), getValue(props, LdapAuthentication, false),
if(getValue(props, LdapAuthentication, false)){ if(getValue(props, LdapAuthentication, false)){
Some(Ldap( Some(Ldap(
getValue(props, LdapHost, ""), getValue(props, LdapHost, ""),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)), getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getOptionValue(props, LdapBindDN, None), getOptionValue(props, LdapBindDN, None),
getOptionValue(props, LdapBindPassword, None), getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""), getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""), getValue(props, LdapUserNameAttribute, ""),
getOptionValue(props, LdapFullNameAttribute, None), getOptionValue(props, LdapFullNameAttribute, None),
getValue(props, LdapMailAddressAttribute, ""), getValue(props, LdapMailAddressAttribute, ""),
getOptionValue[Boolean](props, LdapTls, None), getOptionValue[Boolean](props, LdapTls, None),
getOptionValue(props, LdapKeystore, None))) getOptionValue(props, LdapKeystore, None)))
} else { } else {
None None
} }
) )
} }
} }
} }
object SystemSettingsService { object SystemSettingsService {
import scala.reflect.ClassTag import scala.reflect.ClassTag
case class SystemSettings( case class SystemSettings(
baseUrl: Option[String], baseUrl: Option[String],
allowAccountRegistration: Boolean, allowAccountRegistration: Boolean,
gravatar: Boolean, gravatar: Boolean,
notification: Boolean, notification: Boolean,
ssh: Boolean, ssh: Boolean,
sshPort: Option[Int], sshPort: Option[Int],
smtp: Option[Smtp], smtp: Option[Smtp],
ldapAuthentication: Boolean, ldapAuthentication: Boolean,
ldap: Option[Ldap]) ldap: Option[Ldap]){
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
case class Ldap( defining(request.getRequestURL.toString){ url =>
host: String, url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
port: Option[Int], }
bindDN: Option[String], }.stripSuffix("/")
bindPassword: Option[String], }
baseDN: String,
userNameAttribute: String, case class Ldap(
fullNameAttribute: Option[String], host: String,
mailAttribute: String, port: Option[Int],
tls: Option[Boolean], bindDN: Option[String],
keystore: Option[String]) bindPassword: Option[String],
baseDN: String,
case class Smtp( userNameAttribute: String,
host: String, fullNameAttribute: Option[String],
port: Option[Int], mailAttribute: String,
user: Option[String], tls: Option[Boolean],
password: Option[String], keystore: Option[String])
ssl: Option[Boolean],
fromAddress: Option[String], case class Smtp(
fromName: Option[String]) host: String,
port: Option[Int],
val DefaultSshPort = 29418 user: Option[String],
val DefaultSmtpPort = 25 password: Option[String],
val DefaultLdapPort = 389 ssl: Option[Boolean],
fromAddress: Option[String],
private val BaseURL = "base_url" fromName: Option[String])
private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar" val DefaultSshPort = 29418
private val Notification = "notification" val DefaultSmtpPort = 25
private val Ssh = "ssh" val DefaultLdapPort = 389
private val SshPort = "ssh.port"
private val SmtpHost = "smtp.host" private val BaseURL = "base_url"
private val SmtpPort = "smtp.port" private val AllowAccountRegistration = "allow_account_registration"
private val SmtpUser = "smtp.user" private val Gravatar = "gravatar"
private val SmtpPassword = "smtp.password" private val Notification = "notification"
private val SmtpSsl = "smtp.ssl" private val Ssh = "ssh"
private val SmtpFromAddress = "smtp.from_address" private val SshPort = "ssh.port"
private val SmtpFromName = "smtp.from_name" private val SmtpHost = "smtp.host"
private val LdapAuthentication = "ldap_authentication" private val SmtpPort = "smtp.port"
private val LdapHost = "ldap.host" private val SmtpUser = "smtp.user"
private val LdapPort = "ldap.port" private val SmtpPassword = "smtp.password"
private val LdapBindDN = "ldap.bindDN" private val SmtpSsl = "smtp.ssl"
private val LdapBindPassword = "ldap.bind_password" private val SmtpFromAddress = "smtp.from_address"
private val LdapBaseDN = "ldap.baseDN" private val SmtpFromName = "smtp.from_name"
private val LdapUserNameAttribute = "ldap.username_attribute" private val LdapAuthentication = "ldap_authentication"
private val LdapFullNameAttribute = "ldap.fullname_attribute" private val LdapHost = "ldap.host"
private val LdapMailAddressAttribute = "ldap.mail_attribute" private val LdapPort = "ldap.port"
private val LdapTls = "ldap.tls" private val LdapBindDN = "ldap.bindDN"
private val LdapKeystore = "ldap.keystore" private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = private val LdapUserNameAttribute = "ldap.username_attribute"
defining(props.getProperty(key)){ value => private val LdapFullNameAttribute = "ldap.fullname_attribute"
if(value == null || value.isEmpty) default private val LdapMailAddressAttribute = "ldap.mail_attribute"
else convertType(value).asInstanceOf[A] private val LdapTls = "ldap.tls"
} private val LdapKeystore = "ldap.keystore"
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
defining(props.getProperty(key)){ value => defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default if(value == null || value.isEmpty) default
else Some(convertType(value)).asInstanceOf[Option[A]] else convertType(value).asInstanceOf[A]
} }
private def convertType[A: ClassTag](value: String) = private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
defining(implicitly[ClassTag[A]].runtimeClass){ c => defining(props.getProperty(key)){ value =>
if(c == classOf[Boolean]) value.toBoolean if(value == null || value.isEmpty) default
else if(c == classOf[Int]) value.toInt else Some(convertType(value)).asInstanceOf[Option[A]]
else value }
}
private def convertType[A: ClassTag](value: String) =
} defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Int]) value.toInt
else value
}
}

View File

@@ -1,9 +1,7 @@
package service package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._ import model._
import simple._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
import util.JGitUtil import util.JGitUtil
@@ -12,7 +10,6 @@ import util.JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.protocol.HTTP
import org.apache.http.NameValuePair import org.apache.http.NameValuePair
trait WebHookService { trait WebHookService {
@@ -20,14 +17,14 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService]) private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String): List[WebHook] = def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] =
Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list
def addWebHookURL(owner: String, repository: String, url :String): Unit = def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.insert(WebHook(owner, repository, url)) WebHooks insert WebHook(owner, repository, url)
def deleteWebHookURL(owner: String, repository: String, url :String): Unit = def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = { def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
import org.json4s._ import org.json4s._
@@ -87,7 +84,7 @@ object WebHookService {
refName, refName,
commits.map { commit => commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false) val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id
WebHookCommit( WebHookCommit(
id = commit.id, id = commit.id,
@@ -103,7 +100,7 @@ object WebHookService {
email = commit.mailAddress email = commit.mailAddress
) )
) )
}.toList, },
WebHookRepository( WebHookRepository(
name = repositoryInfo.name, name = repositoryInfo.name,
url = repositoryInfo.httpUrl, url = repositoryInfo.httpUrl,

View File

@@ -1,307 +1,278 @@
package service package service
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils import util._
import util._ import _root_.util.ControlUtil._
import _root_.util.ControlUtil._ import org.eclipse.jgit.treewalk.CanonicalTreeParser
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser} import org.eclipse.jgit.lib._
import org.eclipse.jgit.lib._ import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry} import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import org.eclipse.jgit.revwalk.RevWalk import java.io.ByteArrayInputStream
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter} import org.eclipse.jgit.patch._
import java.io.ByteArrayInputStream import org.eclipse.jgit.api.errors.PatchFormatException
import org.eclipse.jgit.patch._ import scala.collection.JavaConverters._
import org.eclipse.jgit.api.errors.PatchFormatException import service.RepositoryService.RepositoryInfo
import scala.collection.JavaConverters._
import scala.Some object WikiService {
import service.RepositoryService.RepositoryInfo
/**
* The model for wiki page.
object WikiService { *
* @param name the page name
/** * @param content the page content
* The model for wiki page. * @param committer the last committer
* * @param time the last modified time
* @param name the page name * @param id the latest commit id
* @param content the page content */
* @param committer the last committer case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
* @param time the last modified time
* @param id the latest commit id /**
*/ * The model for wiki page history.
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) *
* @param name the page name
/** * @param committer the committer the committer
* The model for wiki page history. * @param message the commit message
* * @param date the commit date
* @param name the page name */
* @param committer the committer the committer case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
* @param message the commit message
* @param date the commit date def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
*/
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") }
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = trait WikiService {
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") import WikiService._
}
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
trait WikiService { LockUtil.lock(s"${owner}/${repository}/wiki"){
import WikiService._ defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
if(!dir.exists){
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = JGitUtil.initRepository(dir)
LockUtil.lock(s"${owner}/${repository}/wiki"){ saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => }
if(!dir.exists){ }
JGitUtil.initRepository(dir) }
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
} /**
} * Returns the wiki page.
} */
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
/** using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
* Returns the wiki page. if(!JGitUtil.isEmpty(git)){
*/ JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => file.committer, file.time, file.commitId)
if(!JGitUtil.isEmpty(git)){ }
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => } else None
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), }
file.committer, file.time, file.commitId) }
}
} else None /**
} * Returns the content of the specified file.
} */
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
/** using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
* Returns the content of the specified file. if(!JGitUtil.isEmpty(git)){
*/ val index = path.lastIndexOf('/')
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = val parentPath = if(index < 0) "." else path.substring(0, index)
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => val fileName = if(index < 0) path else path.substring(index + 1)
if(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/') JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
val parentPath = if(index < 0) "." else path.substring(0, index) git.getRepository.open(file.id).getBytes
val fileName = if(index < 0) path else path.substring(index + 1) }
} else None
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => }
git.getRepository.open(file.id).getBytes
} /**
} else None * Returns the list of wiki page names.
} */
def getWikiPageList(owner: String, repository: String): List[String] = {
/** using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
* Returns the list of wiki page names. JGitUtil.getFileList(git, "master", ".")
*/ .filter(_.name.endsWith(".md"))
def getWikiPageList(owner: String, repository: String): List[String] = { .map(_.name.stripSuffix(".md"))
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => .sortBy(x => x)
JGitUtil.getFileList(git, "master", ".") }
.filter(_.name.endsWith(".md")) }
.map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x) /**
} * Reverts specified changes.
} */
def revertWikiPage(owner: String, repository: String, from: String, to: String,
/** committer: model.Account, pageName: Option[String]): Boolean = {
* Reverts specified changes.
*/ case class RevertInfo(operation: String, filePath: String, source: String)
def revertWikiPage(owner: String, repository: String, from: String, to: String,
committer: model.Account, pageName: Option[String]): Boolean = { try {
LockUtil.lock(s"${owner}/${repository}/wiki"){
case class RevertInfo(operation: String, filePath: String, source: String) using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
try { val reader = git.getRepository.newObjectReader
LockUtil.lock(s"${owner}/${repository}/wiki"){ val oldTreeIter = new CanonicalTreeParser
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
val reader = git.getRepository.newObjectReader val newTreeIter = new CanonicalTreeParser
val oldTreeIter = new CanonicalTreeParser newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
val newTreeIter = new CanonicalTreeParser pageName match {
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) case Some(x) => diff.getNewPath == x + ".md"
case None => true
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => }
pageName match { }
case Some(x) => diff.getNewPath == x + ".md"
case None => true val patch = using(new java.io.ByteArrayOutputStream()){ out =>
} val formatter = new DiffFormatter(out)
} formatter.setRepository(git.getRepository)
formatter.format(diffs.asJava)
val patch = using(new java.io.ByteArrayOutputStream()){ out => new String(out.toByteArray, "UTF-8")
val formatter = new DiffFormatter(out) }
formatter.setRepository(git.getRepository)
formatter.format(diffs.asJava) val p = new Patch()
new String(out.toByteArray, "UTF-8") p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
} if(!p.getErrors.isEmpty){
throw new PatchFormatException(p.getErrors())
val p = new Patch() }
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8"))) val revertInfo = (p.getFiles.asScala.map { fh =>
if(!p.getErrors.isEmpty){ fh.getChangeType match {
throw new PatchFormatException(p.getErrors()) case DiffEntry.ChangeType.MODIFY => {
} val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("")
val revertInfo = (p.getFiles.asScala.map { fh => val applied = PatchUtil.apply(source, patch, fh)
fh.getChangeType match { if(applied != null){
case DiffEntry.ChangeType.MODIFY => { Seq(RevertInfo("ADD", fh.getNewPath, applied))
val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("") } else Nil
val applied = PatchUtil.apply(source, patch, fh) }
if(applied != null){ case DiffEntry.ChangeType.ADD => {
Seq(RevertInfo("ADD", fh.getNewPath, applied)) val applied = PatchUtil.apply("", patch, fh)
} else Nil if(applied != null){
} Seq(RevertInfo("ADD", fh.getNewPath, applied))
case DiffEntry.ChangeType.ADD => { } else Nil
val applied = PatchUtil.apply("", patch, fh) }
if(applied != null){ case DiffEntry.ChangeType.DELETE => {
Seq(RevertInfo("ADD", fh.getNewPath, applied)) Seq(RevertInfo("DELETE", fh.getNewPath, ""))
} else Nil }
} case DiffEntry.ChangeType.RENAME => {
case DiffEntry.ChangeType.DELETE => { val applied = PatchUtil.apply("", patch, fh)
Seq(RevertInfo("DELETE", fh.getNewPath, "")) if(applied != null){
} Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
case DiffEntry.ChangeType.RENAME => { } else {
val applied = PatchUtil.apply("", patch, fh) Seq(RevertInfo("DELETE", fh.getOldPath, ""))
if(applied != null){ }
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied)) }
} else { case _ => Nil
Seq(RevertInfo("DELETE", fh.getOldPath, "")) }
} }).flatten
}
case _ => Nil if(revertInfo.nonEmpty){
} val builder = DirCache.newInCore.builder()
}).flatten val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
if(revertInfo.nonEmpty){
val builder = DirCache.newInCore.builder() JGitUtil.processTree(git, headId){ (path, tree) =>
val inserter = git.getRepository.newObjectInserter() if(revertInfo.find(x => x.filePath == path).isEmpty){
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
using(new RevWalk(git.getRepository)){ revWalk => }
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId)) revertInfo.filter(_.operation == "ADD").foreach { x =>
treeWalk.setRecursive(true) builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
while(treeWalk.next){ }
val path = treeWalk.getPathString builder.finish()
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(revertInfo.find(x => x.filePath == path).isEmpty){ JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) pageName match {
} case Some(x) => s"Revert ${from} ... ${to} on ${x}"
} case None => s"Revert ${from} ... ${to}"
} })
} }
}
revertInfo.filter(_.operation == "ADD").foreach { x => }
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8")))) true
} } catch {
builder.finish() case e: Exception => {
e.printStackTrace()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, false
pageName match { }
case Some(x) => s"Revert ${from} ... ${to} on ${x}" }
case None => s"Revert ${from} ... ${to}" }
})
} /**
} * Save the wiki page.
} */
true def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
} catch { content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
case e: Exception => { LockUtil.lock(s"${owner}/${repository}/wiki"){
e.printStackTrace() using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
false val builder = DirCache.newInCore.builder()
} val inserter = git.getRepository.newObjectInserter()
} val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
} var created = true
var updated = false
/** var removed = false
* Save the wiki page.
*/ if(headId != null){
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, JGitUtil.processTree(git, headId){ (path, tree) =>
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = { if(path == currentPageName + ".md" && currentPageName != newPageName){
LockUtil.lock(s"${owner}/${repository}/wiki"){ removed = true
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => } else if(path != newPageName + ".md"){
val builder = DirCache.newInCore.builder() builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
val inserter = git.getRepository.newObjectInserter() } else {
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") created = false
var created = true updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
var updated = false }
var removed = false }
}
if(headId != null){
using(new RevWalk(git.getRepository)){ revWalk => if(created || updated || removed){
using(new TreeWalk(git.getRepository)){ treeWalk => builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
val index = treeWalk.addTree(revWalk.parseTree(headId)) builder.finish()
treeWalk.setRecursive(true) val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
while(treeWalk.next){ if(message.trim.length == 0) {
val path = treeWalk.getPathString if(removed){
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) s"Rename ${currentPageName} to ${newPageName}"
if(path == currentPageName + ".md" && currentPageName != newPageName){ } else if(created){
removed = true s"Created ${newPageName}"
} else if(path != newPageName + ".md"){ } else {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) s"Updated ${newPageName}"
} else { }
created = false } else {
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) message
} })
}
} Some(newHeadId.getName)
} } else None
} }
}
if(created || updated || removed){ }
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish() /**
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, * Delete the wiki page.
if(message.trim.length == 0) { */
if(removed){ def deleteWikiPage(owner: String, repository: String, pageName: String,
s"Rename ${currentPageName} to ${newPageName}" committer: String, mailAddress: String, message: String): Unit = {
} else if(created){ LockUtil.lock(s"${owner}/${repository}/wiki"){
s"Created ${newPageName}" using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
} else { val builder = DirCache.newInCore.builder()
s"Updated ${newPageName}" val inserter = git.getRepository.newObjectInserter()
} val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
} else { var removed = false
message
}) JGitUtil.processTree(git, headId){ (path, tree) =>
if(path != pageName + ".md"){
Some(newHeadId) builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else None } else {
} removed = true
} }
} }
if(removed){
/** builder.finish()
* Delete the wiki page. JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
*/ }
def deleteWikiPage(owner: String, repository: String, pageName: String, }
committer: String, mailAddress: String, message: String): Unit = { }
LockUtil.lock(s"${owner}/${repository}/wiki"){ }
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder() }
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
removed = true
}
}
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
}
}
}
}
}
}

View File

@@ -1,165 +1,210 @@
package servlet package servlet
import java.io.File import java.io.File
import java.sql.{DriverManager, Connection} import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent} import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory
object AutoUpdate { import plugin.PluginUpdateJob
/** object AutoUpdate {
* Version of GitBucket
* /**
* @param majorVersion the major version * Version of GitBucket
* @param minorVersion the minor version *
*/ * @param majorVersion the major version
case class Version(majorVersion: Int, minorVersion: Int){ * @param minorVersion the minor version
*/
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version]) case class Version(majorVersion: Int, minorVersion: Int){
/** private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
* Execute update/MAJOR_MINOR.sql to update schema to this version.
* If corresponding SQL file does not exist, this method do nothing. /**
*/ * Execute update/MAJOR_MINOR.sql to update schema to this version.
def update(conn: Connection): Unit = { * If corresponding SQL file does not exist, this method do nothing.
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" */
def update(conn: Connection): Unit = {
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in => val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
if(in != null){
val sql = IOUtils.toString(in, "UTF-8") using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
using(conn.createStatement()){ stmt => if(in != null){
logger.debug(sqlPath + "=" + sql) val sql = IOUtils.toString(in, "UTF-8")
stmt.executeUpdate(sql) using(conn.createStatement()){ stmt =>
} logger.debug(sqlPath + "=" + sql)
} stmt.executeUpdate(sql)
} }
} }
}
/** }
* MAJOR.MINOR
*/ /**
val versionString = s"${majorVersion}.${minorVersion}" * MAJOR.MINOR
} */
val versionString = s"${majorVersion}.${minorVersion}"
/** }
* The history of versions. A head of this sequence is the current BitBucket version.
*/ /**
val versions = Seq( * The history of versions. A head of this sequence is the current BitBucket version.
Version(1, 12), */
Version(1, 11), val versions = Seq(
Version(1, 10), new Version(2, 1),
Version(1, 9), new Version(2, 0){
Version(1, 8), override def update(conn: Connection): Unit = {
Version(1, 7), import eu.medsea.mimeutil.{MimeUtil2, MimeType}
Version(1, 6),
Version(1, 5), val mimeUtil = new MimeUtil2()
Version(1, 4), mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
new Version(1, 3){
override def update(conn: Connection): Unit = { super.update(conn)
super.update(conn) using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
// Fix wiki repository configuration while(rs.next){
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
while(rs.next){ if(dir.exists && dir.isDirectory){
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => dir.listFiles.foreach { file =>
defining(git.getRepository.getConfig){ config => if(file.getName.indexOf('.') < 0){
if(!config.getBoolean("http", "receivepack", false)){ val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
config.setBoolean("http", null, "receivepack", true) if(mimeType.startsWith("image/")){
config.save file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
} }
} }
} }
} }
} }
} }
}, }
Version(1, 2), }
Version(1, 1), },
Version(1, 0), Version(1, 13),
Version(0, 0) Version(1, 12),
) Version(1, 11),
Version(1, 10),
/** Version(1, 9),
* The head version of BitBucket. Version(1, 8),
*/ Version(1, 7),
val headVersion = versions.head Version(1, 6),
Version(1, 5),
/** Version(1, 4),
* The version file (GITBUCKET_HOME/version). new Version(1, 3){
*/ override def update(conn: Connection): Unit = {
lazy val versionFile = new File(GitBucketHome, "version") super.update(conn)
// Fix wiki repository configuration
/** using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
* Returns the current version from the version file. while(rs.next){
*/ using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
def getCurrentVersion(): Version = { defining(git.getRepository.getConfig){ config =>
if(versionFile.exists){ if(!config.getBoolean("http", "receivepack", false)){
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { config.setBoolean("http", null, "receivepack", true)
case Array(majorVersion, minorVersion) => { config.save
versions.find { v => }
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt }
}.getOrElse(Version(0, 0)) }
} }
case _ => Version(0, 0) }
} }
} else Version(0, 0) },
} Version(1, 2),
Version(1, 1),
} Version(1, 0),
Version(0, 0)
/** )
* Update database schema automatically in the context initializing.
*/ /**
class AutoUpdateListener extends ServletContextListener { * The head version of BitBucket.
import AutoUpdate._ */
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) val headVersion = versions.head
override def contextInitialized(event: ServletContextEvent): Unit = { /**
val datadir = event.getServletContext.getInitParameter("gitbucket.home") * The version file (GITBUCKET_HOME/version).
if(datadir != null){ */
System.setProperty("gitbucket.home", datadir) lazy val versionFile = new File(GitBucketHome, "version")
}
org.h2.Driver.load() /**
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true") * Returns the current version from the version file.
*/
logger.debug("Start schema update") def getCurrentVersion(): Version = {
defining(getConnection(event.getServletContext)){ conn => if(versionFile.exists){
try { FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
defining(getCurrentVersion()){ currentVersion => case Array(majorVersion, minorVersion) => {
if(currentVersion == headVersion){ versions.find { v =>
logger.debug("No update") v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
} else if(!versions.contains(currentVersion)){ }.getOrElse(Version(0, 0))
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") }
} else { case _ => Version(0, 0)
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) }
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") } else Version(0, 0)
conn.commit() }
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
} }
}
} catch { /**
case ex: Throwable => { * Update database schema automatically in the context initializing.
logger.error("Failed to schema update", ex) */
ex.printStackTrace() class AutoUpdateListener extends ServletContextListener {
conn.rollback() import org.quartz.impl.StdSchedulerFactory
} import org.quartz.JobBuilder._
} import org.quartz.TriggerBuilder._
} import org.quartz.SimpleScheduleBuilder._
logger.debug("End schema update") import AutoUpdate._
}
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
def contextDestroyed(sce: ServletContextEvent): Unit = { private val scheduler = StdSchedulerFactory.getDefaultScheduler
// Nothing to do.
} override def contextInitialized(event: ServletContextEvent): Unit = {
val datadir = event.getServletContext.getInitParameter("gitbucket.home")
private def getConnection(servletContext: ServletContext): Connection = if(datadir != null){
DriverManager.getConnection( System.setProperty("gitbucket.home", datadir)
servletContext.getInitParameter("db.url"), }
servletContext.getInitParameter("db.user"), org.h2.Driver.load()
servletContext.getInitParameter("db.password")) event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
} logger.debug("Start schema update")
defining(getConnection(event.getServletContext)){ conn =>
try {
defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){
logger.debug("No update")
} else if(!versions.contains(currentVersion)){
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
}
}
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
}
}
logger.debug("End schema update")
logger.debug("Starting plugin system...")
plugin.PluginSystem.init()
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
scheduler.shutdown()
}
private def getConnection(servletContext: ServletContext): Connection =
DriverManager.getConnection(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
}

View File

@@ -3,7 +3,9 @@ package servlet
import javax.servlet._ import javax.servlet._
import javax.servlet.http._ import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService} import service.{SystemSettingsService, AccountService, RepositoryService}
import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import slick.jdbc.JdbcBackend
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import util.Keys import util.Keys
@@ -20,7 +22,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
def destroy(): Unit = {} def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
val request = req.asInstanceOf[HttpServletRequest] implicit val request = req.asInstanceOf[HttpServletRequest]
val response = res.asInstanceOf[HttpServletResponse] val response = res.asInstanceOf[HttpServletResponse]
val wrappedResponse = new HttpServletResponseWrapper(response){ val wrappedResponse = new HttpServletResponseWrapper(response){
@@ -38,9 +40,12 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
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) => getWritableUser(username, password, repository) match {
request.setAttribute(Keys.Request.UserName, username) case Some(account) => {
chain.doFilter(req, wrappedResponse) request.setAttribute(Keys.Request.UserName, account.userName)
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
} }
case _ => requireAuth(response) case _ => requireAuth(response)
} }
@@ -61,10 +66,11 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
} }
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
(implicit session: JdbcBackend#Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match { authenticate(loadSystemSettings(), username, password) match {
case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account)) case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case None => false case _ => None
} }
private def requireAuth(response: HttpServletResponse): Unit = { private def requireAuth(response: HttpServletResponse): Unit = {

View File

@@ -16,6 +16,8 @@ import service._
import WebHookService._ import WebHookService._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import service.IssuesService.IssueSearchCondition
import slick.jdbc.JdbcBackend
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -63,7 +65,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
@@ -75,12 +77,16 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "") val repository = paths(2).stripSuffix(".git")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request))) defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
} }
receivePack receivePack
} }
@@ -89,14 +95,30 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: JdbcBackend#Session)
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
existIds = JGitUtil.getAllCommitIds(git)
}
} catch {
case ex: Exception => {
logger.error(ex.toString, ex)
throw ex
}
}
}
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try { try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command => commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
@@ -110,26 +132,26 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
} }
} }
// Extract new commit and apply issue comment // Retrieve all issue count in the repository
val newCommits = if(commits.size > 1000){ val issueCount =
val existIds = getAllCommitIds(owner, repository) countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
commits.flatMap { commit => countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
if(!existIds.contains(commit.id)){
createIssueComment(commit)
Some(commit)
} else None
}
} else {
commits.flatMap { commit =>
if(!existsCommitId(owner, repository, commit.id)){
createIssueComment(commit)
Some(commit)
} else None
}
}
// batch insert all new commit id // Extract new commit and apply issue comment
insertAllCommitIds(owner, repository, newCommits.map(_.id)) val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
val newCommits = commits.flatMap { commit =>
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) {
pushedIds.add(commit.id)
createIssueComment(commit)
// close issues
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
}
}
Some(commit)
} else None
}
// record activity // record activity
if(refName(1) == "heads"){ if(refName(1) == "heads"){
@@ -157,14 +179,6 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
} }
} }
// close issues
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach { commit =>
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
}
}
// call web hook // call web hook
getWebHookURLs(owner, repository) match { getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -204,14 +218,18 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
private def updatePullRequests(branch: String) = private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){ if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git => using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
git.fetch Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true)) .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
.call .call
val commitIdTo = git.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
updateCommitIdTo(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo) val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
pullreq.userName, pullreq.repositoryName, pullreq.branch,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
} }
} }
} }

View File

@@ -0,0 +1,81 @@
package servlet
import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils
import twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.Account
import util.{JGitUtil, Keys}
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
(req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session =>
val path = req.asInstanceOf[HttpServletRequest].getRequestURI
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res)
}
}
}
}
}
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse): Boolean = {
plugin.PluginSystem.globalActions.find(_.path == path).map { action =>
val result = action.function(request, response)
result match {
case x: String => {
response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(Html(x))
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
case x => {
// TODO returns as JSON?
response.setContentType("application/json; charset=UTF-8")
}
}
true
} getOrElse false
}
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: model.simple.Session): Boolean = {
val elements = path.split("/")
if(elements.length > 3){
val owner = elements(1)
val name = elements(2)
val remain = elements.drop(3).mkString("/", "/", "")
getRepository(owner, name, "").flatMap { repository => // TODO fill baseUrl
plugin.PluginSystem.repositoryActions.find(_.path == remain).map { action =>
val result = action.function(request, response)
result match {
case x: String => {
response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(x))) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
case x => {
// TODO returns as JSON?
response.setContentType("application/json; charset=UTF-8")
}
}
true
}
} getOrElse false
} else false
}
}

View File

@@ -1,15 +1,16 @@
package servlet package servlet
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener} import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
import app.FileUploadControllerBase import org.apache.commons.io.FileUtils
import util.Directory._
/** /**
* Removes session associated temporary files when session is destroyed. * Removes session associated temporary files when session is destroyed.
*/ */
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase { class SessionCleanupListener extends HttpSessionListener {
def sessionCreated(se: HttpSessionEvent): Unit = {} def sessionCreated(se: HttpSessionEvent): Unit = {}
def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession) def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId))
} }

View File

@@ -1,38 +1,45 @@
package servlet package servlet
import javax.servlet._ import javax.servlet._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import util.Keys
/**
* 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 {
private val logger = LoggerFactory.getLogger(classOf[TransactionFilter])
private val logger = LoggerFactory.getLogger(classOf[TransactionFilter])
def init(config: FilterConfig) = {}
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
// assets don't need transaction if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){
chain.doFilter(req, res) // assets don't need transaction
} else { chain.doFilter(req, res)
Database(req.getServletContext) withTransaction { } else {
logger.debug("begin transaction") Database(req.getServletContext) withTransaction { session =>
chain.doFilter(req, res) logger.debug("begin transaction")
logger.debug("end transaction") req.setAttribute(Keys.Request.DBSession, session)
} chain.doFilter(req, res)
} logger.debug("end transaction")
} }
}
} }
object Database { }
def apply(context: ServletContext): scala.slick.session.Database =
scala.slick.session.Database.forURL(context.getInitParameter("db.url"), object Database {
context.getInitParameter("db.user"),
context.getInitParameter("db.password")) def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database =
} slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"),
context.getInitParameter("db.password"))
def getSession(req: ServletRequest): slick.jdbc.JdbcBackend#Session =
req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
}

View File

@@ -12,7 +12,7 @@ import servlet.{Database, CommitLogHook}
import service.{AccountService, RepositoryService, SystemSettingsService} import service.{AccountService, RepositoryService, SystemSettingsService}
import org.eclipse.jgit.errors.RepositoryNotFoundException import org.eclipse.jgit.errors.RepositoryNotFoundException
import javax.servlet.ServletContext import javax.servlet.ServletContext
import model.profile.simple.Session
object GitCommand { object GitCommand {
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
@@ -27,11 +27,11 @@ abstract class GitCommand(val context: ServletContext, val owner: String, val re
protected var out: OutputStream = null protected var out: OutputStream = null
protected var callback: ExitCallback = null protected var callback: ExitCallback = null
protected def runTask(user: String): Unit protected def runTask(user: String)(implicit session: Session): Unit
private def newTask(user: String): Runnable = new Runnable { private def newTask(user: String): Runnable = new Runnable {
override def run(): Unit = { override def run(): Unit = {
Database(context) withTransaction { Database(context) withTransaction { implicit session =>
try { try {
runTask(user) runTask(user)
callback.onExit(0) callback.onExit(0)
@@ -71,7 +71,8 @@ abstract class GitCommand(val context: ServletContext, val owner: String, val re
this.in = in this.in = in
} }
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo): Boolean = protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
(implicit session: Session): Boolean =
getAccountByUserName(username) match { getAccountByUserName(username) match {
case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account)) case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account))
case None => false case None => false
@@ -82,7 +83,7 @@ abstract class GitCommand(val context: ServletContext, val owner: String, val re
class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName) class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with RepositoryService with AccountService { with RepositoryService with AccountService {
override protected def runTask(user: String): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){ if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git => using(Git.open(getRepositoryDir(owner, repoName))) { git =>
@@ -99,14 +100,16 @@ class GitUploadPack(context: ServletContext, owner: String, repoName: String, ba
class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName) class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with SystemSettingsService with RepositoryService with AccountService { with SystemSettingsService with RepositoryService with AccountService {
override protected def runTask(user: String): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(isWritableUser(user, repositoryInfo)){ if(isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git => using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository val repository = git.getRepository
val receive = new ReceivePack(repository) val receive = new ReceivePack(repository)
if(!repoName.endsWith(".wiki")){ if(!repoName.endsWith(".wiki")){
receive.setPostReceiveHook(new CommitLogHook(owner, repoName, user, baseUrl)) val hook = new CommitLogHook(owner, repoName, user, baseUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
} }
receive.receive(in, out, err) receive.receive(in, out, err)
} }

View File

@@ -10,7 +10,7 @@ import javax.servlet.ServletContext
class PublicKeyAuthenticator(context: ServletContext) extends PublickeyAuthenticator with SshKeyService { class PublicKeyAuthenticator(context: ServletContext) extends PublickeyAuthenticator with SshKeyService {
override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = { override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = {
Database(context) withTransaction { Database(context) withTransaction { implicit session =>
getPublicKeys(username).exists { sshKey => getPublicKeys(username).exists { sshKey =>
SshUtil.str2PublicKey(sshKey.publicKey) match { SshUtil.str2PublicKey(sshKey.publicKey) match {
case Some(publicKey) => key.equals(publicKey) case Some(publicKey) => key.equals(publicKey)

View File

@@ -31,6 +31,7 @@ object SshServer {
def stop() = { def stop() = {
if(active.compareAndSet(true, false)){ if(active.compareAndSet(true, false)){
server.stop(true) server.stop(true)
logger.info("SSH Server is stopped.")
} }
} }
@@ -45,16 +46,17 @@ object SshServer {
*/ */
class SshServerListener extends ServletContextListener with SystemSettingsService { class SshServerListener extends ServletContextListener with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[SshServerListener])
override def contextInitialized(sce: ServletContextEvent): Unit = { override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings() val settings = loadSystemSettings()
if(settings.ssh){ if(settings.ssh){
if(settings.baseUrl.isEmpty){ settings.baseUrl match {
// TODO use logger? case None =>
println("Could not start SshServer because the baseUrl is not configured.") logger.error("Could not start SshServer because the baseUrl is not configured.")
} else { case Some(baseUrl) =>
SshServer.start(sce.getServletContext, SshServer.start(sce.getServletContext,
settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
settings.baseUrl.get)
} }
} }
} }

View File

@@ -14,7 +14,7 @@ object SshUtil {
// TODO RFC 4716 Public Key is not supported... // TODO RFC 4716 Public Key is not supported...
val parts = key.split(" ") val parts = key.split(" ")
if (parts.size < 2) { if (parts.size < 2) {
logger.debug(s"Invalid PublicKey Format: key") logger.debug(s"Invalid PublicKey Format: ${key}")
return None return None
} }
try { try {
@@ -28,6 +28,9 @@ object SshUtil {
} }
} }
def fingerPrint(key: String): String = KeyUtils.getFingerPrint(str2PublicKey(key).get) def fingerPrint(key: String): Option[String] = str2PublicKey(key) match {
case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey))
case None => None
}
} }

View File

@@ -37,15 +37,4 @@ object ControlUtil {
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T = def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release() try f(treeWalk) finally treeWalk.release()
// def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
// try {
// f(ref)
// } finally {
// val refUpdate = git.getRepository.updateRef(ref.getDestination)
// refUpdate.setForceUpdate(true)
// refUpdate.delete()
// }
// }
} }

View File

@@ -29,24 +29,14 @@ object Directory {
}).getAbsolutePath }).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = s"${GitBucketHome}/repositories" val RepositoryHome = s"${GitBucketHome}/repositories"
val DatabaseHome = s"${GitBucketHome}/data" val DatabaseHome = s"${GitBucketHome}/data"
/** val PluginHome = s"${GitBucketHome}/plugins"
* Repository names of the specified user.
*/ val TemporaryHome = s"${GitBucketHome}/tmp"
def getRepositories(owner: String): List[String] =
defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
if(dir.exists){
dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList
} else {
Nil
}
}
/** /**
* Substance directory of the repository. * Substance directory of the repository.
@@ -54,16 +44,33 @@ object Directory {
def getRepositoryDir(owner: String, repository: String): File = def getRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.git") new File(s"${RepositoryHome}/${owner}/${repository}.git")
/**
* Directory for files which are attached to issue.
*/
def getAttachedDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}/issues")
/** /**
* Directory for uploaded files by the specified user. * Directory for uploaded files by the specified user.
*/ */
def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files") def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
/**
* Root of temporary directories for the upload file.
*/
def getTemporaryDir(sessionId: String): File =
new File(s"${TemporaryHome}/_upload/${sessionId}")
/** /**
* 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"${GitBucketHome}/tmp/${owner}/${repository}") new File(s"${TemporaryHome}/${owner}/${repository}")
/**
* Root of plugin cache directory. Plugin repositories are cloned into this directory.
*/
def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
/** /**
* 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.

View File

@@ -4,9 +4,10 @@ import org.apache.commons.io.FileUtils
import java.net.URLConnection import java.net.URLConnection
import java.io.File import java.io.File
import util.ControlUtil._ import util.ControlUtil._
import scala.util.Random
object FileUtil { object FileUtil {
def getMimeType(name: String): String = def getMimeType(name: String): String =
defining(URLConnection.getFileNameMap()){ fileNameMap => defining(URLConnection.getFileNameMap()){ fileNameMap =>
fileNameMap.getContentTypeFor(name) match { fileNameMap.getContentTypeFor(name) match {
@@ -26,32 +27,12 @@ object FileUtil {
} }
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000) def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0) def isText(content: Array[Byte]): Boolean = !content.contains(0)
// def createZipFile(dest: File, dir: File): Unit = { def generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString
// def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
// dir.listFiles.map { file =>
// if(file.isFile){
// out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
// out.write(FileUtils.readFileToByteArray(file))
// out.closeArchiveEntry
// } else if(file.isDirectory){
// addDirectoryToZip(out, file, path + "/" + file.getName)
// }
// }
// }
//
// using(new ZipArchiveOutputStream(dest)){ out =>
// addDirectoryToZip(out, dir, dir.getName)
// }
// }
def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
if(i >= 0) path.substring(i + 1) else path
}
def getExtension(name: String): String = def getExtension(name: String): String =
name.lastIndexOf('.') match { name.lastIndexOf('.') match {

View File

@@ -2,6 +2,8 @@ package util
import scala.util.matching.Regex import scala.util.matching.Regex
import scala.util.control.Exception._ import scala.util.control.Exception._
import slick.jdbc.JdbcBackend
import servlet.Database
import javax.servlet.http.{HttpSession, HttpServletRequest} import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
@@ -9,6 +11,9 @@ import javax.servlet.http.{HttpSession, HttpServletRequest}
*/ */
object Implicits { object Implicits {
// Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit class RichSeq[A](seq: Seq[A]) { 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)

View File

@@ -35,7 +35,11 @@ object JGitUtil {
* @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, commitCount: Int, branchList: List[String], tags: List[TagInfo]) case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
def this(owner: String, name: String, baseUrl: String) = {
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
}
}
/** /**
* The file data for the file list of the repository viewer. * The file data for the file list of the repository viewer.
@@ -92,8 +96,9 @@ object JGitUtil {
* *
* @param viewType "image", "large" or "other" * @param viewType "image", "large" or "other"
* @param content the string content * @param content the string content
* @param charset the character encoding
*/ */
case class ContentInfo(viewType: String, content: Option[String]) case class ContentInfo(viewType: String, content: Option[String], charset: Option[String])
/** /**
* The tag data. * The tag data.
@@ -137,7 +142,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10000).sum val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
RepositoryInfo( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -145,12 +150,12 @@ object JGitUtil {
commitCount, commitCount,
// branches // branches
git.branchList.call.asScala.map { ref => git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "") ref.getName.stripPrefix("refs/heads/")
}.toList, }.toList,
// tags // tags
git.tagList.call.asScala.map { ref => git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId) val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName) TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList }.toList
) )
} catch { } catch {
@@ -189,7 +194,7 @@ object JGitUtil {
val targetPath = walker.getPathString val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){ if((path + "/").startsWith(targetPath)){
true true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){ } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){
stopRecursive = true stopRecursive = true
treeWalk.setRecursive(false) treeWalk.setRecursive(false)
true true
@@ -371,7 +376,12 @@ object JGitUtil {
if(commits.length >= 2){ if(commits.length >= 2){
// not initial commit // not initial commit
val oldCommit = commits(1) val oldCommit = if(revCommit.getParentCount >= 2) {
// merge commit
revCommit.getParents.head
} else {
commits(1)
}
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName)) (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
} else { } else {
@@ -480,7 +490,7 @@ object JGitUtil {
} }
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId, def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
fullName: String, mailAddress: String, message: String): String = { fullName: String, mailAddress: String, message: String): ObjectId = {
val newCommit = new CommitBuilder() val newCommit = new CommitBuilder()
newCommit.setCommitter(new PersonIdent(fullName, mailAddress)) newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
newCommit.setAuthor(new PersonIdent(fullName, mailAddress)) newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
@@ -498,7 +508,7 @@ object JGitUtil {
refUpdate.setNewObjectId(newHeadId) refUpdate.setNewObjectId(newHeadId)
refUpdate.update() refUpdate.update()
newHeadId.getName newHeadId
} }
/** /**
@@ -549,6 +559,26 @@ object JGitUtil {
} }
} }
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
} else {
// binary
ContentInfo("binary", None, None)
}
} else {
// image or large
ContentInfo(viewer, None, None)
}
}
/** /**
* Get object content of the given object id as byte array from the Git repository. * Get object content of the given object id as byte array from the Git repository.
* *
@@ -570,4 +600,42 @@ object JGitUtil {
case e: MissingObjectException => None case e: MissingObjectException => None
} }
/**
* Returns all commit id in the specified repository.
*/
def getAllCommitIds(git: Git): Seq[String] = if(isEmpty(git)) {
Nil
} else {
val existIds = new scala.collection.mutable.ListBuffer[String]()
val i = git.log.all.call.iterator
while(i.hasNext){
existIds += i.next.name
}
existIds.toSeq
}
def processTree(git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => Unit) = {
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(id))
treeWalk.setRecursive(true)
while(treeWalk.next){
f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
}
}
}
}
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
def getForkedCommitId(oldGit: Git, newGit: Git,
userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
defining(getAllCommitIds(oldGit)){ existIds =>
getCommitLogs(newGit, requestBranch, true) { commit =>
existIds.contains(commit.name) && getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
}
} }

View File

@@ -61,6 +61,11 @@ object Keys {
*/ */
object Request { object Request {
/**
* Request key for the Slick Session.
*/
val DBSession = "DB_SESSION"
/** /**
* Request key for the Ajax request flag. * Request key for the Ajax request flag.
*/ */

View File

@@ -47,11 +47,11 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""), keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed." error = "User LDAP Authentication Failed."
){ conn => ){ conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(LDAPUserInfo( case Some(mailAddress) => Right(LDAPUserInfo(
userName = userName, userName = getUserNameFromMailAddress(userName),
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, fullNameAttribute) findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}.getOrElse(userName), }.getOrElse(userName),
mailAddress = mailAddress)) mailAddress = mailAddress))
case None => Left("Can't find mail address.") case None => Left("Can't find mail address.")
@@ -59,6 +59,13 @@ object LDAPUtil {
} }
} }
private def getUserNameFromMailAddress(userName: String): String = {
(userName.indexOf('@') match {
case i if i >= 0 => userName.substring(0, i)
case i => userName
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
}
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String) private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = { (f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) { if (tls) {
@@ -123,15 +130,15 @@ object LDAPUtil {
} }
} }
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None } else None
} }
private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] = private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None } else None

View File

@@ -1,116 +1,117 @@
package util package util
import scala.concurrent._ import scala.concurrent._
import ExecutionContext.Implicits.global import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import app.Context import app.Context
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database import servlet.Database
import SystemSettingsService.Smtp import SystemSettingsService.Smtp
import _root_.util.ControlUtil.defining import _root_.util.ControlUtil.defining
import model.profile.simple.Session
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) trait Notifier extends RepositoryService with AccountService with IssuesService {
(msg: String => String)(implicit context: Context): Unit def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
( protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
// individual repository's owner (
issue.userName :: // individual repository's owner
// collaborators issue.userName ::
getCollaborators(issue.userName, issue.repositoryName) ::: // collaborators
// participants getCollaborators(issue.userName, issue.repositoryName) :::
issue.openedUserName :: // participants
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) issue.openedUserName ::
) getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
.distinct )
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded .distinct
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) ) .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
}
}
object Notifier {
// TODO We want to be able to switch to mock. object Notifier {
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { // TODO We want to be able to switch to mock.
case settings if settings.notification => new Mailer(settings.smtp.get) def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case _ => new MockMailer case settings if settings.notification => new Mailer(settings.smtp.get)
} case _ => new MockMailer
}
def msgIssue(url: String) = (content: String) => s"""
|${content}<br/> def msgIssue(url: String) = (content: String) => s"""
|--<br/> |${content}<br/>
|<a href="${url}">View it on GitBucket</a> |--<br/>
""".stripMargin |<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgPullRequest(url: String) = (content: String) => s"""
|${content}<hr/> def msgPullRequest(url: String) = (content: String) => s"""
|View, comment on, or merge it at:<br/> |${content}<hr/>
|<a href="${url}">${url}</a> |View, comment on, or merge it at:<br/>
""".stripMargin |<a href="${url}">${url}</a>
""".stripMargin
def msgComment(url: String) = (content: String) => s"""
|${content}<br/> def msgComment(url: String) = (content: String) => s"""
|--<br/> |${content}<br/>
|<a href="${url}">View it on GitBucket</a> |--<br/>
""".stripMargin |<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgStatus(url: String) = (content: String) => s"""
|${content} <a href="${url}">#${url split('/') last}</a> def msgStatus(url: String) = (content: String) => s"""
""".stripMargin |${content} <a href="${url}">#${url split('/') last}</a>
} """.stripMargin
}
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer]) class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context) = { def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
val database = Database(context.request.getServletContext) (msg: String => String)(implicit context: Context) = {
val database = Database(context.request.getServletContext)
val f = future {
// TODO Can we use the Database Session in other than Transaction Filter? val f = future {
database withSession { // TODO Can we use the Database Session in other than Transaction Filter?
getIssue(r.owner, r.name, issueId.toString) foreach { issue => database withSession { implicit session =>
defining( getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
s"[${r.name}] ${issue.title} (#${issueId})" -> defining(
msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => s"[${r.name}] ${issue.title} (#${issueId})" ->
recipients(issue) { to => msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
val email = new HtmlEmail recipients(issue) { to =>
email.setHostName(smtp.host) val email = new HtmlEmail
email.setSmtpPort(smtp.port.get) email.setHostName(smtp.host)
smtp.user.foreach { user => email.setSmtpPort(smtp.port.get)
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) smtp.user.foreach { user =>
} email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
smtp.ssl.foreach { ssl => }
email.setSSLOnConnect(ssl) smtp.ssl.foreach { ssl =>
} email.setSSLOnConnect(ssl)
smtp.fromAddress }
.map (_ -> smtp.fromName.orNull) smtp.fromAddress
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) .map (_ -> smtp.fromName.orNull)
.foreach { case (address, name) => .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
email.setFrom(address, name) .foreach { case (address, name) =>
} email.setFrom(address, name)
email.setCharset("UTF-8") }
email.setSubject(subject) email.setCharset("UTF-8")
email.setHtmlMsg(msg) email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
} email.addTo(to).send
} }
} }
} }
"Notifications Successful." }
} "Notifications Successful."
f onSuccess { }
case s => logger.debug(s) f onSuccess {
} case s => logger.debug(s)
f onFailure { }
case t => logger.error("Notifications Failed.", t) f onFailure {
} case t => logger.error("Notifications Failed.", t)
} }
} }
class MockMailer extends Notifier { }
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) class MockMailer extends Notifier {
(msg: String => String)(implicit context: Context): Unit = {} def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit = {}
} }

View File

@@ -10,7 +10,7 @@ trait Validations {
*/ */
def identifier: Constraint = new Constraint(){ def identifier: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){ if(!value.matches("[a-zA-Z0-9\\-_.]+")){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.") Some(s"${name} starts with invalid character.")

View File

@@ -17,7 +17,7 @@ trait AvatarImageProvider { self: RequestCache =>
// by user name // by user name
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && context.settings.gravatar){ if(account.image.isEmpty && context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {
s"""${context.path}/${account.userName}/_avatar""" s"""${context.path}/${account.userName}/_avatar"""
} }
@@ -28,13 +28,13 @@ trait AvatarImageProvider { self: RequestCache =>
// by mail address // by mail address
getAccountByMailAddress(mailAddress).map { account => getAccountByMailAddress(mailAddress).map { account =>
if(account.image.isEmpty && context.settings.gravatar){ if(account.image.isEmpty && context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {
s"""${context.path}/${account.userName}/_avatar""" s"""${context.path}/${account.userName}/_avatar"""
} }
} getOrElse { } getOrElse {
if(context.settings.gravatar){ if(context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {
s"""${context.path}/_unknown/_avatar""" s"""${context.path}/_unknown/_avatar"""
} }
@@ -42,9 +42,9 @@ trait AvatarImageProvider { self: RequestCache =>
} }
if(tooltip){ if(tooltip){
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""") Html(s"""<img src="${src}" class="${if(size > 20){"avatar"} else {"avatar-mini"}}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else { } else {
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""") Html(s"""<img src="${src}" class="${if(size > 20){"avatar"} else {"avatar-mini"}}" style="width: ${size}px; height: ${size}px;" />""")
} }
} }

View File

@@ -45,7 +45,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
(text, text) (text, text)
} }
val url = repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){ if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label) new Rendering(url, label)
@@ -89,7 +89,8 @@ class GitBucketHtmlSerializer(
) with LinkConverter with RequestCache { ) with LinkConverter with RequestCache {
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("<a target=\"_blank\" href=\"").print(fixUrl(url)).print("\">")
.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
printer.print('<').print('a') printer.print('<').print('a')
@@ -101,10 +102,10 @@ class GitBucketHtmlSerializer(
} }
private def fixUrl(url: String): String = { private def fixUrl(url: String): String = {
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")){ if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){
url url
} else { } else {
repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
} }
} }

View File

@@ -15,6 +15,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/ */
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
/**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
*/
def datetimeRFC3339(date: Date): String = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(date).replaceAll("(\\d\\d)(\\d\\d)$","$1:$2")
/** /**
* Format java.util.Date to "yyyy-MM-dd". * Format java.util.Date to "yyyy-MM-dd".
*/ */
@@ -27,6 +32,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def plural(count: Int, singular: String, plural: String = ""): String = def plural(count: Int, singular: String, plural: String = ""): String =
if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] =
Seq(
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context))
)
def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1)
/** /**
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
@@ -34,6 +47,21 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
val fileNameLower = filePath.reverse.head.toLowerCase
renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match {
case Some((_, handler)) => handler(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context)
case None => Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
}
}
/** /**
* Returns &lt;img&gt; which displays the avatar icon for the given user name. * Returns &lt;img&gt; which displays the avatar icon for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings. * This method looks up Gravatar if avatar icon has not been configured in user settings.
@@ -135,6 +163,44 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/ */
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**
* Returns file type for AceEditor.
*/
def editorType(fileName: String): String = {
fileName.toLowerCase match {
case x if(x.endsWith(".bat")) => "batchfile"
case x if(x.endsWith(".java")) => "java"
case x if(x.endsWith(".scala")) => "scala"
case x if(x.endsWith(".js")) => "javascript"
case x if(x.endsWith(".css")) => "css"
case x if(x.endsWith(".md")) => "markdown"
case x if(x.endsWith(".html")) => "html"
case x if(x.endsWith(".xml")) => "xml"
case x if(x.endsWith(".c")) => "c_cpp"
case x if(x.endsWith(".cpp")) => "c_cpp"
case x if(x.endsWith(".coffee")) => "coffee"
case x if(x.endsWith(".ejs")) => "ejs"
case x if(x.endsWith(".hs")) => "haskell"
case x if(x.endsWith(".json")) => "json"
case x if(x.endsWith(".jsp")) => "jsp"
case x if(x.endsWith(".jsx")) => "jsx"
case x if(x.endsWith(".cl")) => "lisp"
case x if(x.endsWith(".clojure")) => "lisp"
case x if(x.endsWith(".lua")) => "lua"
case x if(x.endsWith(".php")) => "php"
case x if(x.endsWith(".py")) => "python"
case x if(x.endsWith(".rdoc")) => "rdoc"
case x if(x.endsWith(".rhtml")) => "rhtml"
case x if(x.endsWith(".ruby")) => "ruby"
case x if(x.endsWith(".sh")) => "sh"
case x if(x.endsWith(".sql")) => "sql"
case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".vbs")) => "vbscript"
case x if(x.endsWith(".yml")) => "yaml"
case _ => "plain_text"
}
}
/** /**
* Implicit conversion to add mkHtml() to Seq[Html]. * Implicit conversion to add mkHtml() to Seq[Html].
*/ */

View File

@@ -2,5 +2,8 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main(account, groupNames, "activity"){ @main(account, groupNames, "activity"){
<div class="pull-right">
<a href="@path/@{account.userName}.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
</div>
@helper.html.activities(activities) @helper.html.activities(activities)
} }

View File

@@ -2,60 +2,62 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Edit your profile"){ @html.main("Edit your profile"){
<div class="row-fluid"> <div class="container">
<div class="span3"> <div class="row-fluid">
@menu("profile", settings.ssh) <div class="span3">
</div> @menu("profile", settings.ssh)
<div class="span9"> </div>
@helper.html.information(info) <div class="span9">
<form action="@url(account.userName)/_edit" method="POST" validate="true"> @helper.html.information(info)
<div class="box"> <form action="@url(account.userName)/_edit" method="POST" validate="true">
<div class="box-header">Profile</div> <div class="box">
<div class="box-content"> <div class="box-header">Profile</div>
<div class="row-fluid"> <div class="box-content">
<div class="span6"> <div class="row-fluid">
@if(account.password.nonEmpty){ <div class="span6">
@if(account.password.nonEmpty){
<fieldset>
<label for="password" class="strong">
Password (input to change password):
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
}
<fieldset> <fieldset>
<label for="password" class="strong"> <label for="fullName" class="strong">Full Name:</label>
Password (input to change password): <input type="text" name="fullName" id="fullName" value="@account.fullName"/>
</label> <span id="error-fullName" class="error"></span>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset> </fieldset>
} <fieldset>
<fieldset> <label for="mailAddress" class="strong">Mail Address:</label>
<label for="fullName" class="strong">Full Name:</label> <input type="text" name="mailAddress" id="mailAddress" value="@account.mailAddress"/>
<input type="text" name="fullName" id="fullName" value="@account.fullName"/> <span id="error-mailAddress" class="error"></span>
<span id="error-fullName" class="error"></span> </fieldset>
</fieldset> <fieldset>
<fieldset> <label for="url" class="strong">URL (optional):</label>
<label for="mailAddress" class="strong">Mail Address:</label> <input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/>
<input type="text" name="mailAddress" id="mailAddress" value="@account.mailAddress"/> <span id="error-url" class="error"></span>
<span id="error-mailAddress" class="error"></span> </fieldset>
</fieldset> </div>
<fieldset> <div class="span6">
<label for="url" class="strong">URL (optional):</label> <fieldset>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/> <label for="avatar" class="strong">Image (optional):</label>
<span id="error-url" class="error"></span> @helper.html.uploadavatar(Some(account))
</fieldset> </fieldset>
</div>
</div> </div>
<div class="span6"> <div style="margin-top: 20px;">
<fieldset> <div class="pull-right">
<label for="avatar" class="strong">Image (optional):</label> <a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
@helper.html.uploadavatar(Some(account)) </div>
</fieldset> <input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.userName)" class="btn">Cancel</a>
</div> </div>
</div> </div>
<div style="margin-top: 20px;">
<div class="pull-right">
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div>
<input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.userName)" class="btn">Cancel</a>
</div>
</div> </div>
</div> </form>
</form> </div>
</div> </div>
} }
<script> <script>

View File

@@ -2,7 +2,7 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(if(account.isEmpty) "Create group" else "Edit group"){ @html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div> <div class="container">
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true"> <form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span5"> <div class="span5">

View File

@@ -3,54 +3,56 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(account.userName){ @html.main(account.userName){
<div class="container-fluid"> <div class="container">
<div class="row-fluid"> <div class="container-fluid">
<div class="span4"> <div class="row-fluid">
<div class="block"> <div class="span4">
<div class="account-image">@avatar(account.userName, 200)</div> <div class="block">
<div class="account-fullname">@account.fullName</div> <div class="account-image">@avatar(account.userName, 200)</div>
<div class="account-username">@account.userName</div> <div class="account-fullname">@account.fullName</div>
</div> <div class="account-username">@account.userName</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="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>
<div class="span8"> <div class="span8">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs" style="margin-bottom: 5px;">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li> <li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){ @if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li> <li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
} else { } else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li> <li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
} }
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){ @if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right"> <li class="pull-right">
<div class="button-group"> <div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a> <a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div> </div>
</li> </li>
} }
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){ @if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
<li class="pull-right"> <li class="pull-right">
<div class="button-group"> <div class="button-group">
<a href="@url(account.userName)/_editgroup" class="btn">Edit Group</a> <a href="@url(account.userName)/_editgroup" class="btn">Edit Group</a>
</div> </div>
</li> </li>
} }
</ul> </ul>
@body @body
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Create your account"){ @html.main("Create your account"){
<div class="container">
<h3>Create your account</h3> <h3>Create your account</h3>
<form action="@path/register" method="POST" validate="true"> <form action="@path/register" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
@@ -45,4 +46,5 @@
<input type="submit" class="btn btn-success" value="Create account"/> <input type="submit" class="btn btn-success" value="Create account"/>
</fieldset> </fieldset>
</form> </form>
</div>
} }

View File

@@ -9,19 +9,24 @@
} else { } else {
@repositories.map { repository => @repositories.map { repository =>
<div class="block"> <div class="block">
<div class="block-header"> <div class="repository-icon">
<a href="@url(repository)">@repository.name</a> @helper.html.repositoryicon(repository, true)
@if(repository.repository.isPrivate){ </div>
<i class="icon-lock"></i> <div class="repository-content">
} <div class="block-header">
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
}
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div> </div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
}
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div> </div>
} }
} }

View File

@@ -2,44 +2,46 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("SSH Keys"){ @html.main("SSH Keys"){
<div class="row-fluid"> <div class="container">
<div class="span3"> <div class="row-fluid">
@menu("ssh", settings.ssh) <div class="span3">
</div> @menu("ssh", settings.ssh)
<div class="span9">
<div class="box">
<div class="box-header">SSH Keys</div>
<div class="box-content">
@if(sshKeys.isEmpty){
No keys
}
@sshKeys.zipWithIndex.map { case (key, i) =>
@if(i != 0){
<hr>
}
<strong>@key.title</strong> (@_root_.ssh.SshUtil.fingerPrint(key.publicKey))
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-mini btn-danger pull-right">Delete</a>
}
</div>
</div> </div>
<form method="POST" action="@path/@account.userName/_ssh" validate="true"> <div class="span9">
<div class="box"> <div class="box">
<div class="box-header">Add an SSH Key</div> <div class="box-header">SSH Keys</div>
<div class="box-content"> <div class="box-content">
<fieldset> @if(sshKeys.isEmpty){
<label for="title" class="strong">Title</label> No keys
<div><span id="error-title" class="error"></span></div> }
<input type="text" name="title" id="title" style="width: 400px;"/> @sshKeys.zipWithIndex.map { case (key, i) =>
</fieldset> @if(i != 0){
<fieldset> <hr>
<label for="publicKey" class="strong">Key</label> }
<div><span id="error-publicKey" class="error"></span></div> <strong>@key.title</strong> (@_root_.ssh.SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
<textarea name="publicKey" id="publicKey" style="width: 600px; height: 250px;"></textarea> <a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-mini btn-danger pull-right">Delete</a>
</fieldset> }
<input type="submit" class="btn btn-success" value="Add"/>
</div> </div>
</div> </div>
</form> <form method="POST" action="@path/@account.userName/_ssh" validate="true">
<div class="box">
<div class="box-header">Add an SSH Key</div>
<div class="box-content">
<fieldset>
<label for="title" class="strong">Title</label>
<div><span id="error-title" class="error"></span></div>
<input type="text" name="title" id="title" style="width: 400px;"/>
</fieldset>
<fieldset>
<label for="publicKey" class="strong">Key</label>
<div><span id="error-publicKey" class="error"></span></div>
<textarea name="publicKey" id="publicKey" style="width: 600px; height: 250px;"></textarea>
</fieldset>
<input type="submit" class="btn btn-success" value="Add"/>
</div>
</div>
</form>
</div>
</div> </div>
</div> </div>
} }

View File

@@ -1,22 +1,27 @@
@(active: String)(body: Html)(implicit context: app.Context) @(active: String)(body: Html)(implicit context: app.Context)
@import context._ @import context._
<div class="row-fluid"> <div class="container">
<div class="span3"> <div class="row-fluid">
<div class="box"> <div class="span3">
<ul class="nav nav-tabs nav-stacked side-menu"> <div class="box">
<li@if(active=="users"){ class="active"}> <ul class="nav nav-tabs nav-stacked side-menu">
<a href="@path/admin/users">User Management</a> <li@if(active=="users"){ class="active"}>
</li> <a href="@path/admin/users">User Management</a>
<li@if(active=="system"){ class="active"}> </li>
<a href="@path/admin/system">System Settings</a> <li@if(active=="system"){ class="active"}>
</li> <a href="@path/admin/system">System Settings</a>
<li> </li>
<a href="@path/console/login.jsp">H2 Console</a> <li@if(active=="plugins"){ class="active"}>
</li> <a href="@path/admin/plugins">Plugins</a>
</ul> </li>
</div> <li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul>
</div>
</div>
<div class="span9">
@body
</div>
</div> </div>
<div class="span9"> </div>
@body
</div>
</div>

View File

@@ -0,0 +1,37 @@
@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("available")
<form action="@path/admin/plugins/_install" method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>@plugin.version</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="install-plugins" class="btn btn-success" value="Install selected plugins"/>
</form>
}
}
<script>
$(function(){
$('#install-plugins').click(function(){
return confirm('Selected plugin will be installed. Are you sure?');
});
});
</script>

View File

@@ -0,0 +1,37 @@
@()(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("JavaScript Console"){
@admin.html.menu("plugins"){
@tab("console")
<form method="POST">
<div class="box">
<div class="box-header">JavaScript Console</div>
<div class="box-content">
<div id="editor" style="width: 100%; height: 400px;"></div>
</div>
</div>
<fieldset>
<input type="button" id="evaluate" class="btn btn-success" value="Evaluate"/>
</fieldset>
</form>
}
}
<script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script>
$(function(){
var editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode("ace/mode/javascript");
$('#evaluate').click(function(){
$.post('@path/admin/plugins/console', {
script: editor.getValue()
}, function(data){
alert('Success: ' + data);
}).fail(function(error){
alert(error.statusText);
});
});
});
</script>

View File

@@ -0,0 +1,47 @@
@(plugins: List[plugin.Plugin],
updatablePlugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("installed")
<form method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>
@plugin.version
@updatablePlugins.find(_.id == plugin.id).map { x =>
(@x.version is available)
}
</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="update-plugins" class="btn btn-success" value="Update selected plugins" formaction="@path/admin/plugins/_update"/>
<input type="submit" id="delete-plugins" class="btn btn-danger" value="Uninstall selected plugins" formaction="@path/admin/plugins/_delete"/>
</form>
}
}
<script>
$(function(){
$('#update-plugins').click(function(){
return confirm('Selected plugin will be updated. Are you sure?');
});
$('#delete-plugins').click(function(){
return confirm('Selected plugin will be removed permanently. Are you sure?');
});
});
</script>

View File

@@ -0,0 +1,9 @@
@(active: String)(implicit context: app.Context)
@import context._
<ul class="nav nav-tabs">
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins/available">Available plugins</a></li>
@*
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
*@
</ul>

View File

@@ -1,71 +1,71 @@
@(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context) @(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(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 class="pull-right" style="margin-bottom: 4px;"> <div class="pull-right" style="margin-bottom: 4px;">
<a href="@path/admin/users/_newuser" 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> <a href="@path/admin/users/_newgroup" class="btn">New Group</a>
</div> </div>
<label for="includeRemoved"> <label for="includeRemoved">
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/> <input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>
Include removed users Include removed users
</label> </label>
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
@users.map { account => @users.map { account =>
<tr> <tr>
<td @if(account.isRemoved){style="background-color: #dddddd;"}> <td @if(account.isRemoved){style="background-color: #dddddd;"}>
<div class="pull-right"> <div class="pull-right">
@if(account.isGroupAccount){ @if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a> <a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
} else { } else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a> <a href="@path/admin/users/@account.userName/_edituser">Edit</a>
} }
</div> </div>
<div class="strong"> <div class="strong">
@avatar(account.userName, 20) @avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a> <a href="@url(account.userName)">@account.userName</a>
@if(account.isGroupAccount){ @if(account.isGroupAccount){
(Group) (Group)
} else { } else {
@if(account.isAdmin){ @if(account.isAdmin){
(Administrator) (Administrator)
} else { } else {
(Normal) (Normal)
} }
} }
@if(account.isGroupAccount){ @if(account.isGroupAccount){
@members(account.userName).map { userName => @members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true) @avatar(userName, 20, tooltip = true)
} }
} }
</div> </div>
<div> <div>
<hr> <hr>
@if(!account.isGroupAccount){ @if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress <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
} }
</div> </div>
<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)
@if(!account.isGroupAccount){ @if(!account.isGroupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime) <span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
} }
</div> </div>
</td> </td>
</tr> </tr>
} }
</table> </table>
} }
} }
<script> <script>
$(function(){ $(function(){
$('#includeRemoved').click(function(){ $('#includeRemoved').click(function(){
location.href = '@path/admin/users?includeRemoved=' + this.checked; location.href = '@path/admin/users?includeRemoved=' + this.checked;
}); });
}); });
</script> </script>

View File

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

View File

@@ -8,41 +8,43 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Your Issues"){ @html.main("Your Issues"){
@dashboard.html.tab("issues") <div class="container">
<div class="row-fluid"> @dashboard.html.tab("issues")
<div class="span3"> <div class="row-fluid">
<ul class="nav nav-pills nav-stacked"> <div class="span3">
<li@if(filter == "all"){ class="active"}> <ul class="nav nav-pills nav-stacked">
<a href="@path/dashboard/issues/repos@condition.toURL"> <li@if(filter == "all"){ class="active"}>
<span class="count-right">@allCount</span> <a href="@path/dashboard/issues/repos@condition.toURL">
In your repositories <span class="count-right">@allCount</span>
</a> In your repositories
</li> </a>
<li@if(filter == "assigned"){ class="active"}> </li>
<a href="@path/dashboard/issues/assigned@condition.toURL"> <li@if(filter == "assigned"){ class="active"}>
<span class="count-right">@assignedCount</span> <a href="@path/dashboard/issues/assigned@condition.toURL">
Assigned to you <span class="count-right">@assignedCount</span>
</a> Assigned to you
</li> </a>
<li@if(filter == "created_by"){ class="active"}> </li>
<a href="@path/dashboard/issues/created_by@condition.toURL"> <li@if(filter == "created_by"){ class="active"}>
<span class="count-right">@createdByCount</span> <a href="@path/dashboard/issues/created_by@condition.toURL">
Created by you <span class="count-right">@createdByCount</span>
</a> Created by you
</li> </a>
</ul> </li>
<hr/> </ul>
<ul class="nav nav-pills nav-stacked small"> <hr/>
@repositories.map { case (owner, name, count) => <ul class="nav nav-pills nav-stacked small">
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}> @repositories.map { case (owner, name, count) =>
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL"> <li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<span class="count-right">@count</span> <a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
@owner/@name <span class="count-right">@count</span>
</a> @owner/@name
</li> </a>
} </li>
</ul> }
</ul>
</div>
@listparts
</div> </div>
@listparts
</div> </div>
} }

View File

@@ -6,35 +6,37 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Your Issues"){ @html.main("Your Issues"){
@dashboard.html.tab("pulls") <div class="container">
<div class="row-fluid"> @dashboard.html.tab("pulls")
<div class="span3"> <div class="row-fluid">
<ul class="nav nav-pills nav-stacked"> <div class="span3">
<li@if(filter == "created_by"){ class="active"}> <ul class="nav nav-pills nav-stacked">
<a href="@path/dashboard/pulls/owned@condition.toURL"> <li@if(filter == "created_by"){ class="active"}>
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span> <a href="@path/dashboard/pulls/owned@condition.toURL">
Yours <span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
</a> Yours
</li> </a>
<li@if(filter == "not_created_by"){ class="active"}> </li>
<a href="@path/dashboard/pulls/public@condition.toURL"> <li@if(filter == "not_created_by"){ class="active"}>
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span> <a href="@path/dashboard/pulls/public@condition.toURL">
Public <span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
</a> Public
</li> </a>
</ul> </li>
<hr/> </ul>
<ul class="nav nav-pills nav-stacked small"> <hr/>
@repositories.map { case (owner, name, count) => <ul class="nav nav-pills nav-stacked small">
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}> @repositories.map { case (owner, name, count) =>
<a href="@path/dashboard/pulls/for/@owner/@name"> <li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<span class="count-right">@count</span> <a href="@path/dashboard/pulls/for/@owner/@name">
@owner/@name <span class="count-right">@count</span>
</a> @owner/@name
</li> </a>
} </li>
</ul> }
</ul>
</div>
@listparts
</div> </div>
@listparts
</div> </div>
} }

View File

@@ -1,9 +1,13 @@
@(active: String = "")(implicit context: app.Context) @(active: String = "")(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li> <li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li> <li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li> <li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
} }
@if(active == ""){
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li>
}
</ul> </ul>

View File

@@ -1,73 +0,0 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@if(repository.commitCount > 0){
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
}
<div class="head">
@if(repository.repository.isPrivate){
<img src="@assets/common/images/repo_private_lg.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork_lg.png"/>
} else {
<img src="@assets/common/images/repo_public_lg.png"/>
}
}
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@defining(repository.repository){ x =>
@if(repository.repository.originRepositoryName.isDefined){
<div class="forked">
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
}
</div>
@repository.repository.description.map { description =>
<p>@description</p>
}
<table class="global-nav box-header">
<tr>
<th class="box-header@if(active=="code"){ active}">
<a href="@url(repository)">Code</a>
</th>
<th class="box-header@if(active=="issues"){ active}">
<a href="@url(repository)/issues">Issues</a>
@if(repository.issueCount > 0){
<span class="badge">@repository.issueCount</span>
}
</th>
<th class="box-header@if(active=="pulls"){ active}">
<a href="@url(repository)/pulls">Pull Requests</a>
@if(repository.pullCount > 0){
<span class="badge">@repository.pullCount</span>
}
</th>
<th class="box-header@if(active=="wiki"){ active}">
<a href="@url(repository)/wiki">Wiki</a>
</th>
<th class="box-header@if(active=="network"){ active}">
<a href="@url(repository)/network/members">Network</a>
</th>
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
<th class="box-header@if(active=="settings"){ active}">
<a href="@url(repository)/settings">Settings</a>
</th>
}
</tr>
</table>
<script type="text/javascript">
$(function(){
$('table.global-nav th.box-header').click(function(){
location.href = $(this).find('a').attr('href');
return false;
});
});
</script>

View File

@@ -1,98 +1,98 @@
@(activities: List[model.Activity])(implicit context: app.Context) @(activities: List[model.Activity])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@if(activities.isEmpty){ @if(activities.isEmpty){
No activity No activity
} else { } else {
@activities.map { activity => @activities.map { activity =>
<div class="block"> <div class="block">
@(activity.activityType match { @(activity.activityType match {
case "open_issue" => detailActivity(activity, "activity-issue.png") case "open_issue" => detailActivity(activity, "activity-issue.png")
case "comment_issue" => detailActivity(activity, "activity-comment.png") case "comment_issue" => detailActivity(activity, "activity-comment.png")
case "close_issue" => detailActivity(activity, "activity-issue-close.png") case "close_issue" => detailActivity(activity, "activity-issue-close.png")
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png") case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
case "open_pullreq" => detailActivity(activity, "activity-merge.png") case "open_pullreq" => detailActivity(activity, "activity-merge.png")
case "merge_pullreq" => detailActivity(activity, "activity-merge.png") case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
case "create_repository" => simpleActivity(activity, "activity-create-repository.png") case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
case "create_branch" => simpleActivity(activity, "activity-branch.png") case "create_branch" => simpleActivity(activity, "activity-branch.png")
case "delete_branch" => simpleActivity(activity, "activity-delete.png") case "delete_branch" => simpleActivity(activity, "activity-delete.png")
case "create_tag" => simpleActivity(activity, "activity-tag.png") case "create_tag" => simpleActivity(activity, "activity-tag.png")
case "delete_tag" => simpleActivity(activity, "activity-delete.png") case "delete_tag" => simpleActivity(activity, "activity-delete.png")
case "fork" => simpleActivity(activity, "activity-fork.png") case "fork" => simpleActivity(activity, "activity-fork.png")
case "push" => customActivity(activity, "activity-commit.png"){ case "push" => customActivity(activity, "activity-commit.png"){
<div class="small activity-message"> <div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){ if(i == 3){
<div>...</div> <div>...</div>
} else { } else {
if(commit.nonEmpty){ if(commit.nonEmpty){
<div> <div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a> <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> <span>{commit.substring(41)}</span>
</div> </div>
} }
} }
}} }}
</div> </div>
} }
case "create_wiki" => customActivity(activity, "activity-wiki.png"){ case "create_wiki" => customActivity(activity, "activity-wiki.png"){
<div class="small activity-message"> <div class="small activity-message">
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>. Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div> </div>
} }
case "edit_wiki" => customActivity(activity, "activity-wiki.png"){ case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
activity.additionalInfo.get.split(":") match { activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) => case Array(pageName, commitId) =>
<div class="small activity-message"> <div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>. Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a> <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a>
</div> </div>
case Array(pageName) => case Array(pageName) =>
<div class="small activity-message"> <div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>. Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
</div> </div>
} }
} }
}) })
</div> </div>
} }
} }
@detailActivity(activity: model.Activity, image: String) = { @detailActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div> <div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
</div> </div>
@activity.additionalInfo.map { additionalInfo => @activity.additionalInfo.map { additionalInfo =>
<div class=" activity-message">@additionalInfo</div> <div class=" activity-message">@additionalInfo</div>
} }
</div> </div>
} }
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = { @customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div> <div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
</div> </div>
@additionalInfo @additionalInfo
</div> </div>
} }
@simpleActivity(activity: model.Activity, image: String) = { @simpleActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-small"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-small"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div> <div>
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
<span class="muted small">@datetime(activity.activityDate)</span> <span class="muted small">@datetime(activity.activityDate)</span>
</div> </div>
</div> </div>
} }

View File

@@ -0,0 +1,27 @@
@(owner: String, repository: String)(textarea: Html)(implicit context: app.Context)
@import context._
<div class="muted attachable">
@textarea
<div class="clickable">Attach images by dragging &amp; dropping, or selecting them.</div>
</div>
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
<script>
$(function(){
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
url: '@path/upload/image/@owner/@repository',
maxFilesize: 10,
acceptedFiles: 'image/*',
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + images);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
}
});
// Adjust clickable area width
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');
});
</script>
}

View File

@@ -1,5 +1,5 @@
@(id: String, value: String, prepend: Boolean = false)(html: Html) @(id: String, value: String)(html: Html)
<div class="input-append@if(prepend){ input-prepend}"> <div class="input-append" style="margin-bottom: 0px;">
@html @html
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span> <span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
</div> </div>

View File

@@ -1,154 +1,105 @@
@(diffs: Seq[util.JGitUtil.DiffInfo], @(diffs: Seq[util.JGitUtil.DiffInfo],
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
newCommitId: Option[String], newCommitId: Option[String],
oldCommitId: Option[String], oldCommitId: Option[String],
showIndex: Boolean)(implicit context: app.Context) showIndex: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType @import org.eclipse.jgit.diff.DiffEntry.ChangeType
@if(showIndex){ @if(showIndex){
<div> <div>
<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") Showing @diffs.size changed @plural(diffs.size, "file")
</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) =>
<li@if(i > 0){ class="border"}> <li@if(i > 0){ class="border"}>
<a href="#diff-@i"> <a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath <img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
} }
@if(diff.changeType == ChangeType.ADD){ @if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath <img src="@assets/common/images/diff_add.png"/> @diff.newPath
} }
@if(diff.changeType == ChangeType.MODIFY){ @if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath <img src="@assets/common/images/diff_edit.png"/> @diff.newPath
} }
@if(diff.changeType == ChangeType.DELETE){ @if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath <img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
} }
</a> </a>
</li> </li>
} }
</ul> </ul>
} }
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
<a name="diff-@i"></a> <a name="diff-@i"></a>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th style="font-weight: normal;" class="box-header"> <th style="font-weight: normal;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath @diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){ @if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@diff.newPath @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
@if(diff.changeType == ChangeType.DELETE){ @if(diff.changeType == ChangeType.DELETE){
@diff.oldPath @diff.oldPath
@if(oldCommitId.isDefined){ @if(oldCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
</th> </th>
</tr> </tr>
<tr> <tr>
<td> <td>
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i"></div> <div id="diffText-@i"></div>
<textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea> <textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea>
<textarea id="oldText-@i" style="display: none;">@diff.oldContent.getOrElse("")</textarea> <textarea id="oldText-@i" style="display: none;">@diff.oldContent.getOrElse("")</textarea>
} else { } else {
Not supported Not supported
} }
</td> </td>
</tr> </tr>
</table> </table>
} }
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<style type="text/css"> <script>
table.inlinediff { $(function(){
width: 100%; @if(showIndex){
} $('#toggle-file-list').click(function(){
$('#commit-file-list').toggle();
table.inlinediff thead { if($(this).val() == 'Show file list'){
display: none; $(this).val('Hide file list');
} } else {
$(this).val('Show file list');
td.insert, td.equal, td.delete { }
width: 100%; });
} }
</style>
<script> @diffs.zipWithIndex.map { case (diff, i) =>
function diffUsingJS(oldTextId, newTextId, outputId) { @if(diff.newContent != None || diff.oldContent != None){
// get the baseText and newText values from the two textboxes, and split them into lines if($('#oldText-@i').length > 0){
var oldText = document.getElementById(oldTextId).value; diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i');
if(oldText == ''){ }
var oldLines = []; }
} else { }
var oldLines = difflib.stringAsLines(oldText); });
}
var newText = document.getElementById(newTextId).value
if(newText == ''){
var newLines = [];
} else {
var newLines = difflib.stringAsLines(newText);
}
// create a SequenceMatcher instance that diffs the two sets of lines
var sm = new difflib.SequenceMatcher(oldLines, newLines);
// get the opcodes from the SequenceMatcher instance
// opcodes is a list of 3-tuples describing what changes should be made to the base text
// in order to yield the new text
var opcodes = sm.get_opcodes();
var diffoutputdiv = document.getElementById(outputId);
while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild);
// build the diff view and add it to the current DOM
diffoutputdiv.appendChild(diffview.buildView({
baseTextLines: oldLines,
newTextLines: newLines,
opcodes: opcodes,
contextSize: 4,
viewType: 1
}));
}
$(function(){
@if(showIndex){
$('#toggle-file-list').click(function(){
$('#commit-file-list').toggle();
if($(this).val() == 'Show file list'){
$(this).val('Hide file list');
} else {
$(this).val('Show file list');
}
});
}
@diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i');
}
}
}
});
</script> </script>

View File

@@ -1,6 +1,6 @@
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html) @(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html)
<div class="btn-group"@if(style.nonEmpty){ style="@style"}> <div class="btn-group"@if(style.nonEmpty){ style="@style"}>
<button class="btn dropdown-toggle@if(mini){ btn-mini}" data-toggle="dropdown"> <button class="btn dropdown-toggle@if(mini){ btn-mini} else { btn-small}" data-toggle="dropdown">
@if(value.isEmpty){ @if(value.isEmpty){
<i class="icon-cog"></i> <i class="icon-cog"></i>
} else { } else {

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