Compare commits

..

443 Commits
1.8 ... 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
takezoe
5317ac5e03 Update README.md 2014-03-29 16:24:06 +09:00
Naoki Takezoe
9df1467ddf Merge pull request #329 from andytait/master
README typo fix.
2014-03-28 14:13:15 +09:00
Andy Tait
df79bd4515 README typo fix. 2014-03-27 10:11:56 +00:00
Naoki Takezoe
cbb14f2ba8 Update README.md 2014-03-25 05:20:03 +09:00
takezoe
1fe649e70f (refs #326)SSH clone URL includes username for logged-in users and it's hidden for non logged-in users. 2014-03-25 05:11:29 +09:00
Naoki Takezoe
0d918add28 Merge pull request #320 from kunigaku/fix_247
Fix #247 servlet.CommitLogHook.onPostReceive takes minutes
2014-03-25 04:51:04 +09:00
Naoki Takezoe
3926c98338 Merge pull request #286 from maliayas/patch-2
Improve code view
2014-03-25 02:06:20 +09:00
kaakaa
3bff6a1949 Implement atom feeds 2014-03-24 23:06:37 +09:00
takezoe
ec0c964ceb Fix #325 2014-03-20 17:50:25 +09:00
Naoki Takezoe
b4fd90c6d3 Merge pull request #323 from r0n22/patch-1
Documentation Change for Large file Sizes
2014-03-20 10:59:11 +09:00
r0n22
7dfd63cfa2 Update README.md
Ran into this issue today.  Added to documentation to let other users know about it.
2014-03-19 14:21:30 -04:00
Naoki Takezoe
a562e5ca14 Merge pull request #319 from jmu/master
Trim LF of version file
2014-03-19 04:04:31 +09:00
Naoki Takezoe
2885eef4ab Merge pull request #321 from xuwei-k/sbt0.13
sbt 0.13
2014-03-19 04:03:15 +09:00
xuwei-k
087297d14c sbt 0.13 2014-03-19 00:48:57 +09:00
kunigaku
6e0fb95ac3 Fix #247 servlet.CommitLogHook.onPostReceive takes minutes
When pushing tags, command.getOldId value is
“0000000000000000000000000000000000000000”.
So, can not get commit logs.
2014-03-18 22:51:15 +09:00
takezoe
61e28146fb Fix TODO 2014-03-16 02:10:48 +09:00
takezoe
40d3f0ef9e (refs #313)Search files which have FileMode.EXECUTABLE_FILE mode not only REGULAR_FILE 2014-03-16 01:55:26 +09:00
takezoe
99db825114 (refs #313)Add files which have FileMode.EXECUTABLE_FILE mode into zip file 2014-03-16 01:53:14 +09:00
takezoe
7341b377fe Fix #314 2014-03-15 23:10:21 +09:00
takezoe
7f78a98de0 Display the first line of commit message on the file list 2014-03-15 18:03:52 +09:00
takezoe
a64207f0ec Focus error field after validation. 2014-03-15 16:37:43 +09:00
takezoe
d86f40e3a2 Small fix 2014-03-15 16:06:49 +09:00
Jiang
b74417f393 trim LF of version file 2014-03-15 10:14:41 +08:00
takezoe
f5883abf04 Use Context#settings instead of loadSystemSettings() 2014-03-15 04:07:31 +09:00
takezoe
02a367fd99 Add SystemSettings to Context properties 2014-03-15 01:24:34 +09:00
takezoe
4870533710 Upgrade scalatra-forms to 0.0.14 2014-03-15 01:04:28 +09:00
takezoe
8170a1b01d (refs #115)Display finger print on the ssh key setting page 2014-03-14 11:39:40 +09:00
takezoe
d1c6c763e2 (refs #115)Add button to toggle http/ssh for wiki repository url box 2014-03-14 10:59:42 +09:00
takezoe
0c683f7243 (refs #115)SSH access to Wiki repository is available 2014-03-14 10:44:38 +09:00
takezoe
63de780527 Remove last '/' in base url 2014-03-14 03:21:35 +09:00
takezoe
c5ccbf2d1f (refs #115)Fix TestCase 2014-03-14 02:57:31 +09:00
takezoe
8777535431 (refs #115)Base URL is required if SSH access is enabled 2014-03-14 02:07:31 +09:00
takezoe
70192ce420 (refs #115)Add new method to get sshUrl to RepositoryInfo 2014-03-13 22:26:15 +09:00
takezoe
02d79cb16a (refs #115)Add url switcher to the repository url box 2014-03-13 16:54:11 +09:00
Tomofumi Tanaka
78ca9b3f1a (refs #115)Use system settings port number 2014-03-13 01:47:28 +09:00
Tomofumi Tanaka
017631e337 (refs #115)Add ShellFactory 2014-03-13 00:56:33 +09:00
Tomofumi Tanaka
f9078dff2c (refs #115)Remove debug code 2014-03-13 00:45:29 +09:00
takezoe
b66381d677 Merge branch 'master' into ssh-access
Conflicts:
	src/main/scala/servlet/GitRepositoryServlet.scala
2014-03-12 22:43:32 +09:00
takezoe
49bf88f7a7 (refs #312)Fix redirection for non git client 2014-03-12 22:39:12 +09:00
Tomofumi Tanaka
f93ceaa91d (refs #115)Fix typo 2014-03-12 01:01:12 +09:00
Tomofumi Tanaka
0fe122dc63 (refs #115)Parse owner and repository correctly 2014-03-12 00:57:03 +09:00
takezoe
3d251fa8ad (refs #115)Disable ssh key setting page if ssh access is disabled 2014-03-11 02:37:02 +09:00
takezoe
af0b52448a (refs #115)Add repository permission checking 2014-03-11 02:30:11 +09:00
takezoe
f78cdb637d (refs #115)Fix TestCase 2014-03-10 01:00:10 +09:00
takezoe
845f2d6faa (refs #115)Start and stop sshd at the system settings 2014-03-10 00:44:25 +09:00
takezoe
525edbab80 (refs #115)Add TODO 2014-03-09 02:27:46 +09:00
takezoe
c422b1c9a5 (refs #115)Small fix for views 2014-03-09 02:12:32 +09:00
takezoe
1043b13228 (refs #115)Small fix for views 2014-03-09 01:48:30 +09:00
takezoe
5e6c33df6c (refs #115)Fix account registration page and add public key deletion 2014-03-09 01:38:23 +09:00
takezoe
9541771703 (refs #115)Use registered public key for authentication 2014-03-09 01:08:04 +09:00
takezoe
f99d37cfad (refs #115)Adding SSH Key form is available 2014-03-09 00:57:00 +09:00
takezoe
0cfe31ccd9 (refs #115)Fix TestCase 2014-03-08 22:00:36 +09:00
takezoe
8fc1a5473b (refs #115)Add table and model to store public keys 2014-03-08 19:26:49 +09:00
takezoe
049b12b908 Merge branch 'master' into ssh-access
Conflicts:
	src/main/scala/servlet/GitRepositoryServlet.scala
2014-03-08 18:59:13 +09:00
takezoe
45f992b2bc (refs #115)ServletContext is passed to Command 2014-03-08 14:57:17 +09:00
takezoe
9e2c66c341 (refs #115)ServletContext is passed to Command 2014-03-08 14:48:58 +09:00
takezoe
2d0f59b6f2 (refs #307)Fix deletion problem for branches which contains '/' 2014-03-08 04:22:55 +09:00
takezoe
fbba29e810 (refs #303)Submodule support 2014-03-08 02:58:49 +09:00
Tomofumi Tanaka
07a108760c (refs #115)Use regex to create git command
And add GitCommandFactory Unit Test Spec
2014-03-06 23:36:01 +09:00
takezoe
b641bfb56a (refs #241)Modify AccountService#getGroupMembers() to returns list of GroupMember instead of Tuple2 2014-03-06 16:17:41 +09:00
takezoe
c65d80bc72 (refs #241)Fix error occurring when member name contains special characters. 2014-03-06 15:51:12 +09:00
takezoe
e0d266bf16 (refs #241)Allow group manager to use repository settings 2014-03-06 15:30:58 +09:00
takezoe
b62f7c5aee Fix TestCase 2014-03-06 14:27:49 +09:00
takezoe
c89f04b926 (refs #241)Fix group editing in the administration console 2014-03-06 12:17:25 +09:00
takezoe
ff8b4b4a88 Move newrepo.scala.html to account package 2014-03-06 11:39:41 +09:00
takezoe
07d63ae63a Add caret to new icon 2014-03-06 11:39:13 +09:00
takezoe
c0f5cb1641 Move group.scala.html to account package 2014-03-06 11:26:58 +09:00
takezoe
50d84835cb Merge CreateController to AccountController 2014-03-06 11:24:28 +09:00
takezoe
8cdf4ef618 Merge branch 'improve-group' 2014-03-06 11:06:19 +09:00
takezoe
eff3a7acb4 (refs #241)Fix group creation / edition form presentation 2014-03-06 11:05:47 +09:00
takezoe
716eddac7b (refs #241)Checkbox for manager is replaced with toggle buttons 2014-03-06 02:46:40 +09:00
takezoe
9b15af3bb7 Update README.md for 1.11.1 release 2014-03-05 22:16:37 +09:00
takezoe
b732e0d55a Fix error when base url is configured. 2014-03-05 22:12:34 +09:00
takezoe
d92a1cee1c Replace Context#path with the base url if it's configured. 2014-03-05 19:18:50 +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
takezoe
c7a2ec8290 (refs #299)Fix redirection path in PullRequestsController 2014-03-05 16:23:27 +09:00
shimamoto
145c155ba5 Remove unnecessary import. 2014-03-05 14:40:52 +09:00
shimamoto
6f9ef32d96 (refs #292) Fix to limit issue result before joins issue labels. 2014-03-05 14:35:49 +09:00
takezoe
aa5b9dbbbd Remove unnecessary code 2014-03-05 04:23:44 +09:00
takezoe
f11be44c02 (refs #330)Return NotFound if specified file does not exist 2014-03-05 04:18:45 +09: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
Tomofumi Tanaka
d46589ad29 (refs #115)Ensure exit status 1 on GitCommand error 2014-03-04 23:19:58 +09:00
Tomofumi Tanaka
09b7e67c52 (refs #110)Correct authentication and CommitHook 2014-03-04 22:49:25 +09:00
Tomofumi Tanaka
79e1abe624 (refs #110)Update apache sshd version
For fix authentication partial bug
2014-03-04 22:42:32 +09:00
Tobias Roeser
3db3bf1b74 Rewrite relative links to reflect the base url of the repo. 2014-03-04 11:38:56 +01:00
takezoe
9bd1f0a492 (refs #241)Remove unnecessary code 2014-03-04 10:53:26 +09:00
takezoe
7a2c82461e (refs #241)Group management improvement is completed in user side 2014-03-04 10:52:37 +09:00
takezoe
21f7888f55 (refs #241)Add validation for create / edit group form. 2014-03-04 10:23:44 +09:00
takezoe
e3fd564efd (refs #241)Work for specifying group manager 2014-03-04 04:25:44 +09:00
takezoe
5cf96134d5 (refs #296)Fix redirection path generation again 2014-03-04 03:25:18 +09:00
takezoe
607c477e7d Add options for remote debugging 2014-03-04 02:39:18 +09:00
takezoe
17920e1195 (refs #198)Allow group editing by group members. 2014-03-03 01:45:00 +09:00
Naoki Takezoe
721454aa90 Merge pull request #294 from eiryu/work
fix typo
2014-03-03 01:21:59 +09:00
takezoe
d870896cfb Rename CreateRepositoryController to CreateController. 2014-03-03 01:21:22 +09:00
takezoe
270eb7cf1d (refs #198)Allow create group by normal users. 2014-03-03 01:17:52 +09:00
eiryu
527fd94145 fix typo 2014-03-03 00:28:48 +09:00
takezoe
04e4572088 Merge branch '1.11-update' 2014-03-03 00:04:18 +09:00
takezoe
0961eb5976 (refs #279)Fix redirect url generation. 2014-03-02 23:55:03 +09:00
Tobias Roeser
0311359922 Merge branch 'master' into asciidoctorj 2014-03-02 09:15:57 +01:00
takezoe
ec09adf03e Merge branch 'odz-closing-issues-via-commit-messages' 2014-03-02 04:49:53 +09:00
takezoe
b031103df8 (refs #218)Separate StringUtil#extractCloseId() to add unit test. 2014-03-02 04:49:14 +09:00
takezoe
7701521a2e Fix to use Exception#ignoring to ignore Exception. 2014-03-02 04:24:24 +09:00
takezoe
0c683d4f75 Merge branch 'closing-issues-via-commit-messages' of https://github.com/odz/gitbucket into odz-closing-issues-via-commit-messages 2014-03-02 04:02:45 +09:00
Naoki Takezoe
200d095034 Merge pull request #290 from lefou/compare-email-caseinsensitive
Compare email adresses case-insensitive.
2014-03-02 01:35:00 +09:00
takezoe
94576a876a (refs #280)Commit count limitation is changed to 10000 from 1000. 2014-03-02 01:23:30 +09:00
takezoe
0fa1922bb0 Small fix to pull request #289 2014-03-02 01:05:42 +09:00
Naoki Takezoe
c557905858 Merge pull request #289 from lefou/description-in-header
Show repository description below the name on repository page.
2014-03-02 00:56:07 +09:00
takezoe
31b21d74b1 Remove --https option. 2014-03-02 00:25:56 +09:00
takezoe
153244c390 Update README.mf for GitBucket 1.11 release 2014-03-01 15:26:10 +09:00
takezoe
e97b5c3c89 (refs #279)Remove --https option because it's possible to substitute in the base url configuration. 2014-03-01 15:24:55 +09:00
takezoe
374893a5ae Apply scala.util.control.Exception to exception handling. 2014-03-01 15:22:58 +09:00
takezoe
17f581f654 (refs #279)Override ScalatraBase#fullUrl() to apply the configured base url to redirection. 2014-03-01 15:19:42 +09:00
takezoe
590b431ec1 Add FlashMapSupport to ControllerBase. 2014-03-01 13:27:36 +09:00
takezoe
98266fe0e1 (refs #279)Fix webhook URL to use the configured base URL. 2014-03-01 13:05:42 +09:00
Tomofumi Tanaka
2e236e90ba (refs #110) Add CommitHook sample
WIP: Some important functions are not implement yet.
2014-03-01 02:09:24 +09:00
Tomofumi Tanaka
c5aee0810c (refs #115) Add PublicKeyAuthenticator sample impl
Auth TODOs

* Fetch user account pubkeys from DB
2014-03-01 01:00:00 +09:00
Tobias Roeser
f13d757976 Compare email adresses case-insensitive. 2014-02-28 16:28:40 +01:00
Tobias Roeser
7a0a62af2d Show repository description below the name on repository page. 2014-02-28 15:25:32 +01:00
Tomofumi Tanaka
ceab1d2fd2 (refs #115) Reorganize ssh sources 2014-02-28 22:41:01 +09: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
Naoki Takezoe
b620307983 Merge pull request #287 from lefou/correct-readme-name
Show the correct name of the readme file
2014-02-27 15:37:55 +09:00
Tomofumi Tanaka
891ca70ade refs #115: Support git access via ssh (1st step)
EXPERIMENTAL

TODOs

* Authentication (PublicKey)
* Publickey management in profile view
* Commit hooks integration (WebHook too)
* Parse command correctly
* Make Configurable many options(enable/disable, port, etc...)
* ShellProcessFactory
* Test, test, test...
2014-02-27 02:05:58 +09:00
Tomofumi Tanaka
9ed2a50d26 Add SSH Service Listener 2014-02-27 02:02:37 +09: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
Tobias Roeser
2c14dfb781 Show the correct name of the readme file (instead of showing always README.md). 2014-02-26 12:09:14 +01:00
Ali Ayas
057c5f073c Improve diff view 2014-02-26 12:09:25 +02:00
Ali Ayas
e902da6595 Improve code view
Now it's more similar to GitHub
2014-02-26 11:40:15 +02:00
odz
8b5414c8f7 Merge branch 'master' into closing-issues-via-commit-messages
Conflicts:
	src/main/scala/app/PullRequestsController.scala
	src/main/scala/servlet/GitRepositoryServlet.scala
2014-02-25 00:07:25 +09:00
Tomofumi Tanaka
c86ece4dc0 Add mina ssh to dependencies 2014-02-24 22:45:45 +09:00
Naoki Takezoe
1f71619b6b Merge pull request #281 from maliayas/patch-1
Apply GitHub diff colors
2014-02-24 12:15:57 +09:00
Naoki Takezoe
5b34b9c795 Update README.md 2014-02-24 02:34:13 +09:00
Naoki Takezoe
99d15899f6 Update README.md 2014-02-24 02:32:19 +09:00
takezoe
c114a8b507 (refs #279)Fix TestCase. 2014-02-24 02:16:18 +09:00
takezoe
0dd37c2481 (refs #279)Fix redirect URL generation in authentication. 2014-02-24 02:14:10 +09:00
Ali Ayas
b5d7c96bba Apply GitHub diff colors
However, jsdifflib is still not a good approach. It requires dumping all the two text to the browser and do the work there. Instead, maybe the diff should be taken from the git itself and highlighting should be applied on that.

OTOH, word level diff would be good when applicable (like GitHub does).
2014-02-23 13:11:10 +02:00
takezoe
a76792ced4 (refs #279)Add configuration to specify the base URL. But still one problem has not been resolved. 2014-02-22 14:20:51 +09:00
takezoe
39091240ff (refs #60)Mentioned issue reference from other issue or issue comment. 2014-02-22 05:13:39 +09:00
takezoe
0ccb753892 (refs #187)Add large icons for repository. 2014-02-22 02:00:49 +09:00
takezoe
63dda84c8b Add Version 1.11 2014-02-21 12:05:15 +09:00
takezoe
7ba1f85d48 (refs #187)Repository icons are updated. 2014-02-21 11:39:02 +09:00
takezoe
bb9a23fe0f Fix TestCase. 2014-02-20 03:40:06 +09:00
takezoe
8536824d7e (refs #231)Fix anchor icon style and apply URL encoding to non-ascii chars in anchor name. 2014-02-19 03:40:39 +09:00
takezoe
78073babe4 (refs #231)Add anchor icon for headlines in Markdown. 2014-02-19 03:19:34 +09:00
Naoki Takezoe
521d15219c Merge pull request #272 from jeffreyolchovy/issue/231
Fix for #231: Generate headline anchor in Wiki pages
2014-02-11 04:53:37 +09:00
Naoki Takezoe
7469a3c349 Merge pull request #270 from rndstr/patch-1
Update new data directory in README.md
2014-02-10 07:50:40 +09:00
Jeffrey Olchovy
153a32e340 (refs #231)Generate anchors for headers in wiki markup 2014-02-08 13:59:27 -05:00
Roland Schilter
f155d4f150 Update new data directory in README.md 2014-02-08 13:11:13 +01:00
takezoe
d683dd2c38 (refs #268)Fix label filter bug when label contains whitespaces. 2014-02-08 07:04:18 +09:00
takezoe
7ebba741a8 (refs #232)Highlight lines which are specified by URL hash. 2014-02-08 06:52:50 +09:00
takezoe
d10f683098 (refs #259)Add underline to h1 and h2 in div.markdown-body. 2014-02-08 05:14:26 +09:00
takezoe
0270133ecf (refs #267)Improve H2 connectivity 2014-02-08 05:06:24 +09:00
takezoe
d7b479d97d (refs #265)Fix label pulldown position. 2014-02-08 05:04:06 +09:00
takezoe
4366c512fe (refs #265)Label editing for the pull request. 2014-02-05 03:07:37 +09:00
Naoki Takezoe
229a773ed2 Merge pull request #262 from shootaroo/jump-line
Add id for line number
2014-02-04 09:41:24 -08:00
takezoe
d882f20436 (refs #254)Change comment action to "delete_branch" from "delete". 2014-02-04 17:20:48 +09:00
takezoe
9d7235af20 (refs #254)Store removed branch name into CONTENT column of COMMENT table. 2014-02-04 17:06:54 +09:00
takezoe
c2eb53d154 (refs #224)Add delete branch button to pull request from same repository. 2014-02-04 09:04:25 +09:00
takezoe
7629e347df (refs #224)Record delete branch activity 2014-02-03 08:06:04 +09:00
takezoe
2764caae29 (refs #224)Add delete branch button 2014-02-03 08:00:43 +09:00
Naoki Takezoe
a87bd2a928 Merge pull request #264 from bati11/257-https-reverse-proxy
Fix #257, "org.scalatra.ForceHttps" set to true, if --https=true
2014-02-01 21:39:01 -08:00
bati11
202c920064 Fix #257, "org.scalatra.ForceHttps" set to true, if --https=true
ScalatraBase.redirect() use "org.scalatra.ForceHttps" in servlet
context init parameter when choice 'http' or 'https'.
2014-02-02 03:17:17 +09:00
Naoki Takezoe
a08316bba0 Update README.md 2014-02-01 17:15:50 +09:00
Naoki Takezoe
520e5ebb7a Update README.md 2014-02-01 17:14:41 +09:00
Naoki Takezoe
5d5a4cacb1 Update README.md 2014-02-01 17:13:18 +09:00
takezoe
b885a1a0d4 (refs #256)If account is already registered but disabled, authentication fails. 2014-02-01 17:05:33 +09:00
takezoe
1705bd3ae9 Merge remote-tracking branch 'origin/master' 2014-02-01 07:08:39 +09:00
takezoe
e87c69f989 (refs #251)Remove BOM from UTF-8 string. 2014-02-01 07:08:03 +09:00
takezoe
1c529eea3d Disable the post commit hook for Wiki repository. 2014-02-01 07:06:17 +09:00
takezoe
738b0cfe9a Add version 1.10. 2014-02-01 06:11:18 +09:00
takezoe
913561cb2a (refs #254)Remove AUTO_SERVER=TRUE for performance issue. 2014-01-30 20:42:53 +09:00
shootaroo
05a91565dc Add id for line number 2014-01-30 15:16:29 +09:00
takezoe
79827efe9b Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-01-28 03:20:25 +09:00
takezoe
8722cd89fc Merge branch 'ldap-fullname' 2014-01-28 03:19:40 +09:00
Naoki Takezoe
52fcc4ad1e Update README.md 2014-01-26 08:43:34 +09:00
takezoe
59a096bfd6 (refs #250)Include repository name in download zip filename. 2014-01-25 05:25:17 +09:00
takezoe
5a1f541e13 (refs #245)Add full name attribute for LDAP authentication. 2014-01-25 05:07:32 +09:00
takezoe
94bd1c6a93 Merge branch 'rename-repository' 2014-01-18 18:05:45 +09:00
takezoe
5b1aef5e52 (refs #101, #102)Put Repository deletion and transfer ownership together to Danger Zone. 2014-01-18 07:06:48 +09:00
takezoe
89bfcdc44e (refs #102)Add validation and auto completion to the transfer user name field 2014-01-18 06:44:39 +09:00
takezoe
fba81138ea (refs #102)Experimental implementation of transfer repository ownership 2014-01-18 04:14:32 +09:00
takezoe
d50e07265e Add unique checking before rename repository. 2014-01-18 03:54:57 +09:00
takezoe
c92891538e Fix Cancel button position in the comment editing form. 2014-01-18 01:52:59 +09:00
takezoe
ccc1e9bc8b (refs #246)Fix issue editing 2014-01-18 01:52:34 +09:00
takezoe
f33b398428 (refs #102)Change for transfer repository owner. 2014-01-16 19:39:14 +09:00
takezoe
226a8af262 Use old home if it exists. 2014-01-15 05:25:18 +09:00
takezoe
ebcc5ab4b1 (refs #101)Update links in activity message also. 2014-01-15 04:35:38 +09:00
takezoe
10e16e8379 Use old home if it exists. 2014-01-15 01:28:15 +09:00
takezoe
df1f3d8a00 (refs #101)Modification to add rename repository name. 2014-01-13 02:09:05 +09:00
takezoe
5e2dfffe25 Use FileUtils#moveDirectory() instead of File#renameTo() 2014-01-12 17:59:02 +09:00
takezoe
897f2ea6dd Update data directory checking condition. 2014-01-12 16:47:23 +09:00
takezoe
3ff39ec578 Merge SignInController into IndexController 2014-01-12 16:16:09 +09:00
takezoe
3d852a535d (refs #244)Change the default data directory to HOME/.gitbucket 2014-01-12 15:41:01 +09:00
takezoe
6f6a61f31a Fix pattern match for webhook. 2014-01-04 17:23:18 +09:00
takezoe
10f54f5790 Ignore .settings directory. 2014-01-04 04:16:51 +09:00
takezoe
0e7280585a Fix refs commit log and web hook. 2014-01-04 04:11:41 +09:00
takezoe
1da7173f27 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-12-28 02:43:57 +09:00
takezoe
1cb1e68a01 Add Version 1.9 2013-12-28 02:43:15 +09:00
Naoki Takezoe
b59c8a5512 Update README.md 2013-12-28 01:49:04 +09:00
Naoki Takezoe
fe63ad0976 Merge pull request #242 from ssogabe/pull-request-messages
Fixed pull request messages.They are a bit different from GitHub
2013-12-21 19:57:50 -08:00
ssogabe
941cb7b851 Fixed pull request messages.They are a bit different from GitHub 2013-12-22 12:28:36 +09:00
takezoe
d1cf0d9fd7 (refs #238)Enable automatic mixed mode of H2. 2013-12-22 02:06:44 +09:00
takezoe
64c2bb4d6b (refs #237)Add link to gitbucket.plist 2013-12-21 05:30:48 +09:00
takezoe
24c9f5c17e (refs #237)Move gitbucket.plist to contrib. 2013-12-21 05:29:52 +09:00
Naoki Takezoe
d368e4e80d Merge pull request #237 from hanxue/osx
Add plist launcher for OS X and installation instructions
2013-12-20 12:26:32 -08:00
Naoki Takezoe
5c0ff84fc4 Merge pull request #233 from shootaroo/fix-style-readme
Fix style markdown table on README.md
2013-12-20 11:51:08 -08:00
Lee Hanxue
502a21b6b6 Add plist launcher for OS X and installation instructions 2013-12-19 17:59:32 +08:00
takezoe
0e9bf59c0f Remove some functions from ControlUtil. 2013-12-15 04:21:39 +09:00
shootaroo
108f9fccdd Fix style markdown table on README.md 2013-12-13 19:28:10 +09:00
takezoe
ac884bd7c3 (refs #196)Fire WebHook in merging pull request from Web GUI. 2013-12-12 04:23:43 +09:00
takezoe
a4cb5c991c Upgrade scalatra-forms to 0.0.11. 2013-12-12 03:22:37 +09:00
takezoe
68f1f55f37 (refs #223)Display GITBUCKET_HOME on the system settings. 2013-12-12 03:17:42 +09:00
Naoki Takezoe
1dc779d5e8 Merge pull request #228 from mcveat/compiler-warnings
Silenced compiler warnings
2013-12-11 09:53:19 -08:00
Naoki Takezoe
f781c7a08c Merge pull request #220 from maliayas/patch-1
Turn off autocomplete on "Add collaborator" form
2013-12-03 12:13:20 -08:00
Naoki Takezoe
a8511a9f39 Merge pull request #221 from drwlrsn/master
Fixes issue #216
2013-12-03 12:08:45 -08:00
Piotr Adamski
47714eec45 Silenced compiler warnings 2013-12-03 18:41:41 +01:00
takezoe
c46e9b2f4d (refs #204)Replace order of ScalatraListener and AutoUpdateListener 2013-12-03 02:16:00 +09:00
Drew Larson
26d579f13f Fixes issue #216
Added a div element to wrap the buttons so they are vertically aligned with each other. Also converted input and a elements to button elements as Bootstrap recommends: http://getbootstrap.com/css/#buttons-tags
2013-12-01 19:56:21 -06:00
Ali Ayas
6556d26742 Turn off autocomplete on "Add collaborator" form
You have already created js autocomplete for that input, so it is good to turn off the browser autocomplete. If there are more forms that have custom autocomplete, this change should be applied to them, too.
2013-12-01 23:55:34 +02:00
Naoki Takezoe
608dce2205 Update README.md 2013-11-30 21:20:12 +09:00
Naoki Takezoe
f86e50c723 Update README.md 2013-11-30 21:04:21 +09:00
odz
dc78dc9b0d Close issues via commit messages 2013-11-30 18:57:19 +09:00
439 changed files with 276167 additions and 6448 deletions

1
.gitignore vendored
View File

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

108
README.md
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,10 +23,9 @@ 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
- Statics - Statistics
- Watch / Star - Watch / Star
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki). If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -35,35 +37,115 @@ Installation
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher. 2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser. 3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options. or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
- --port=[NUMBER] - --port=[NUMBER]
- --prefix=[CONTEXTPATH] - --prefix=[CONTEXTPATH]
- --host=[HOSTNAME] - --host=[HOSTNAME]
- --https=true
- --gitbucket.home=[DATA_DIR] - --gitbucket.home=[DATA_DIR]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk. To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
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
#### 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/`
Run the following commands in `Terminal` to
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
Release Notes Release Notes
-------- --------
### 1.8 - COMMING SOON! ### 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
- SSH repository access is available
- Allow users can create and management their groups
- Git submodule support
- Close issues via commit messages
- Show repository description below the name on repository page
- Fix presentation of the source viewer
- Upgrade to sbt 0.13
- Fix some bugs
### 1.11.1 - 06 Mar 2014
- Bug fix
### 1.11 - 01 Mar 2014
- Base URL for redirection, notification and repository URL box is configurable
- Remove ```--https``` option because it's possible to substitute in the base url
- Headline anchor is available for Markdown contents such as Wiki page
- Improve H2 connectivity
- Label is available for pull requests not only issues
- Delete branch button is added
- Repository icons are updated
- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
- Display reference to issue from others in comment list
- Fix some bugs
### 1.10 - 01 Feb 2014
- Rename repository
- Transfer repository owner
- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
- Add LDAP display name attribute
- Response performance improvement
- Fix some bugs
### 1.9 - 28 Dec 2013
- Display GITBUCKET_HOME on the system settings page
- Fix some bugs
### 1.8 - 30 Nov 2013
- Add user and group deletion - Add user and group deletion
- Improve pull request performance - Improve pull request performance
- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request) - Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
- LDAP StartTLS support - LDAP StartTLS support
- Hard wrap for Markdown - Enable hard wrapping in Markdown
- Add new some options to specify the data directory - Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
- Fix some bugs - Fix some bugs
### 1.7 - 26 Oct 2013 ### 1.7 - 26 Oct 2013
- Support working on Java6 in embedded Jetty mode - Support working on Java6 in embedded Jetty mode
- Add ```--host``` option to bind specified host name in embedded Jetty mode - Add `--host` option to bind specified host name in embedded Jetty mode
- Add ```--https=true``` option to use https in embedded Jetty mode - Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
- Add full name as user property - Add full name as user property
- Change link color for absent Wiki pages - Change link color for absent Wiki pages
- Add ZIP download button to the repository viewer tab - Add ZIP download button to the repository viewer tab

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

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>gitbucket</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-Dmail.smtp.starttls.enable=true</string>
<string>-jar</string>
<string>gitbucket.war</string>
<string>--host=127.0.0.1</string>
<string>--port=8080</string>
<string>--https=true</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

View File

@@ -4,9 +4,6 @@
# Server port # Server port
#GITBUCKET_PORT=8080 #GITBUCKET_PORT=8080
# Force HTTPS scheme
#GITBUCKET_HTTPS=false
# Data directory (GITBUCKET_HOME/gitbucket) # Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket #GITBUCKET_HOME=/var/lib/gitbucket

View File

@@ -39,9 +39,6 @@ start() {
if [ $GITBUCKET_HOST ]; then if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}" START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
fi
# Run the Java process # Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 & GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &

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,18 +34,18 @@
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="629.30023" inkscape:cx="482.58197"
inkscape:cy="281.44758" 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="705" inkscape:window-height="715"
inkscape:window-x="-8" inkscape:window-x="-8"
inkscape:window-y="-8" inkscape:window-y="-8"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:snap-global="true" inkscape:snap-global="false"
inkscape:snap-grids="false" inkscape:snap-grids="false"
inkscape:snap-page="false" inkscape:snap-page="false"
inkscape:snap-bbox="true" inkscape:snap-bbox="true"
@@ -746,6 +756,948 @@
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z" d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
id="rect2995-0-2-7-7" id="rect2995-0-2-7-7"
inkscape:connector-curvature="0" /> inkscape:connector-curvature="0" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083"
width="170.93134"
height="207.72536"
x="38.526306"
y="1299.8645" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-7"
width="171.86089"
height="167.53221"
x="38.061527"
y="1300.4821" />
<rect
id="rect2995-0-4"
y="1301.3412"
x="42.553577"
height="163.64935"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0"
y="1321.9025"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9"
y="1356.7848"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9-4"
y="1391.6671"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9-4-8"
y="1426.5494"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-8"
y="1482.7141"
x="70.149086"
height="30.541632"
width="42.755199"
style="fill:#b3b3b3;stroke:none" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(1.0346242,0,0,1.5150471,-165.95814,-2.7851671)"
inkscape:transform-center-x="-2.5637799" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-2"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(-0.93510984,0,0,1.5150471,326.24502,-2.7851671)"
inkscape:transform-center-x="3.5106467" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-4"
width="170.93134"
height="207.72536"
x="280.50113"
y="1299.152" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-7-5"
width="171.86087"
height="167.53221"
x="280.03638"
y="1299.7695" />
<rect
id="rect2995-0-4-5"
y="1300.6287"
x="284.52841"
height="163.64934"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-8-5"
y="1482.0016"
x="312.12393"
height="30.541632"
width="42.755199"
style="fill:#b3b3b3;stroke:none" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-27"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(1.0346242,0,0,1.5150471,76.016678,-3.496726)"
inkscape:transform-center-x="-3.8842459"
inkscape:transform-center-y="-1.5464308e-005" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-2-6"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(-0.93510984,0,0,1.5150471,568.21986,-3.496726)"
inkscape:transform-center-x="5.318797"
inkscape:transform-center-y="-1.5464308e-005" />
<rect
id="rect2995-0-4-5-7"
y="1392.2405"
x="365.67133"
height="58.049755"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-6"
y="1319.5453"
x="326.67615"
height="49.632401"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-8"
y="1179.0293"
x="-767.54126"
height="58.049755"
width="29.769083"
style="fill:#b3b3b3;stroke:none"
transform="matrix(0.68860063,-0.7251408,0.7251408,0.68860063,0,0)" />
<rect
id="rect2995-0-4-5-7-6-9"
y="1319.5453"
x="403.28595"
height="49.632404"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-8-2"
y="623.14606"
x="-1287.8975"
height="55.681484"
width="28.564859"
style="fill:#b3b3b3;stroke:none"
transform="matrix(-0.68607628,-0.72752961,-0.72274236,0.69111755,0,0)" />
<rect
style="color:#000000;fill:#ffffff;stroke:#b3b3b3;stroke-width:7.29121827999999980;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;fill-opacity:1"
id="rect3083-7-5-7"
width="172.98204"
height="125.03616"
x="529.78156"
y="1383.6165" />
<rect
id="rect2995-0-4-5-9"
y="1385.3533"
x="663.37042"
height="123.85819"
width="38.18644"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5"
y="1401.4539"
x="552.03174"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5-4"
y="1437.4023"
x="551.16083"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5-4-3"
y="1473.7642"
x="551.16083"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<path
style="fill:none;stroke:#b3b3b3;stroke-width:23.0681076;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 558.62308,1380.7989 0,-45.237 c 0,0 13.52904,-35.6384 56.38304,-36.1894 40.81922,-0.5248 55.47363,34.6931 55.47363,34.6931 l 0.17276,48.4719"
id="path4310"
inkscape:connector-curvature="0"
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: 38 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -1 +1 @@
sbt.version=0.12.3 sbt.version=0.13.1

View File

@@ -1,8 +1,6 @@
import sbt._ import sbt._
import Keys._ import Keys._
import org.scalatra.sbt._ import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
import sbt.ScalaVersion
import twirl.sbt.TwirlPlugin._ import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -17,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,
@@ -25,25 +24,28 @@ object MyBuild extends Build {
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/" "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
), ),
scalacOptions := Seq("-deprecation"), scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.5", "org.json4s" %% "json4s-jackson" % "3.2.5",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.8", "jp.sf.amateras" %% "scalatra-forms" % "0.0.14",
"commons-io" % "commons-io" % "2.4", "commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1", "org.pegdown" % "pegdown" % "1.4.1",
"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",
"com.typesafe.slick" %% "slick" % "1.0.1", "org.apache.sshd" % "apache-sshd" % "0.11.0",
"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",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")), "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test" "junit" % "junit" % "4.11" % "test"
), ),
EclipseKeys.withSource := true, EclipseKeys.withSource := true,

View File

@@ -1,9 +1,11 @@
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0") addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1") addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0") addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1") resolvers += "spray repo" at "http://repo.spray.io"
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2") addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")

Binary file not shown.

BIN
sbt-launch-0.13.1.jar Normal file

Binary file not shown.

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0 set SCRIPT_DIR=%~dp0
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %* java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %*

2
sbt.sh
View File

@@ -1 +1 @@
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@" java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.1.jar "$@"

View File

@@ -25,8 +25,6 @@ public class JettyLauncher {
port = Integer.parseInt(dim[1]); port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) { } else if(dim[0].equals("--prefix")) {
contextPath = dim[1]; contextPath = dim[1];
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
forceHttps = true;
} else if(dim[0].equals("--gitbucket.home")){ } else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]); System.setProperty("gitbucket.home", dim[1]);
} }
@@ -36,7 +34,7 @@ public class JettyLauncher {
Server server = new Server(); Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps); SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) { if(host != null) {
connector.setHost(host); connector.setHost(host);
} }
@@ -53,25 +51,12 @@ public class JettyLauncher {
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml"); context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server); context.setServer(server);
context.setWar(location.toExternalForm()); context.setWar(location.toExternalForm());
if (forceHttps) {
context.setInitParameter("org.scalatra.ForceHttps", "true");
}
server.setHandler(context); server.setHandler(context);
server.start(); server.start();
server.join(); server.join();
} }
} }
class HttpsSupportConnector extends SelectChannelConnector {
private boolean forceHttps;
public HttpsSupportConnector(boolean forceHttps) {
this.forceHttps = forceHttps;
}
@Override
public void customize(final EndPoint endpoint, final Request request) throws IOException {
if (this.forceHttps) {
request.setScheme("https");
super.customize(endpoint, request);
}
}
}

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,11 @@
ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;
CREATE TABLE SSH_KEY (
USER_NAME VARCHAR(100) NOT NULL,
SSH_KEY_ID INT AUTO_INCREMENT,
TITLE VARCHAR(100) NOT NULL,
PUBLIC_KEY TEXT NOT NULL
);
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID);
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);

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/*")
@@ -17,11 +19,9 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new IndexController, "/") context.mount(new IndexController, "/")
context.mount(new SearchController, "/") context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*")
context.mount(new DashboardController, "/*") context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*") context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")
context.mount(new AccountController, "/*") context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*") context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*") context.mount(new WikiController, "/*")
@@ -30,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

@@ -1,20 +1,27 @@
package app package app
import service._ import service._
import util.{FileUtil, OneselfAuthenticator} import util._
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import util.ControlUtil._
import util.Implicits._
import ssh.SshUtil
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import model.GroupMember
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with SystemSettingsService with AccountService with RepositoryService with ActivityService with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport { trait AccountControllerBase extends AccountManagementControllerBase {
self: SystemSettingsService with AccountService with RepositoryService with ActivityService self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator => with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String]) url: Option[String], fileId: Option[String])
@@ -22,6 +29,8 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String, case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean) url: Option[String], fileId: Option[String], clearImage: Boolean)
case class SshKeyForm(title: String, publicKey: String)
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "password" -> trim(label("Password" , text(required, maxlength(20)))),
@@ -40,6 +49,45 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
"clearImage" -> trim(label("Clear image" , boolean())) "clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply) )(AccountEditForm.apply)
val sshKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim(label("Key" , text(required, validPublicKey)))
)(SshKeyForm.apply)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean()))
)(EditGroupForm.apply)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkRepositoryForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/** /**
* Displays user information. * Displays user information.
*/ */
@@ -54,18 +102,30 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
getActivitiesByUser(userName, true)) getActivitiesByUser(userName, true))
// Members // Members
case "members" if(account.isGroupAccount) => case "members" if(account.isGroupAccount) => {
_root_.account.html.members(account, getGroupMembers(account.userName)) val members = getGroupMembers(account.userName)
_root_.account.html.members(account, members.map(_.userName),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
// Repositories // Repositories
case _ => case _ => {
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 }))
}
} }
} 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 =>
@@ -79,7 +139,9 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
get("/:userName/_edit")(oneselfOnly { get("/:userName/_edit")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound getAccountByUserName(userName).map { x =>
account.html.edit(x, flash.get("info"))
} getOrElse NotFound
}) })
post("/:userName/_edit", editForm)(oneselfOnly { form => post("/:userName/_edit", editForm)(oneselfOnly { form =>
@@ -119,22 +181,254 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
redirect("/") redirect("/")
}) })
get("/:userName/_ssh")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
account.html.ssh(x, getPublicKeys(x.userName))
} getOrElse NotFound
})
post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
val userName = params("userName")
addPublicKey(userName, form.title, form.publicKey)
redirect(s"/${userName}/_ssh")
})
get("/:userName/_ssh/delete/:id")(oneselfOnly {
val userName = params("userName")
val sshKeyId = params("id").toInt
deletePublicKey(userName, sshKeyId)
redirect(s"/${userName}/_ssh")
})
get("/register"){ get("/register"){
if(loadSystemSettings().allowAccountRegistration){ if(context.settings.allowAccountRegistration){
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){
redirect("/") redirect("/")
} else { } else {
account.html.edit(None, None) account.html.register()
} }
} else NotFound } else NotFound
} }
post("/register", newForm){ form => post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){ if(context.settings.allowAccountRegistration){
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url) createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false) updateImage(form.userName, form.fileId, false)
redirect("/signin") redirect("/signin")
} else NotFound } else NotFound
} }
get("/groups/new")(usersOnly {
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
})
post("/groups/new", newGroupForm)(usersOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect(s"/${form.groupName}")
})
get("/:groupName/_editgroup")(managersOnly {
defining(params("groupName")){ groupName =>
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
get("/:groupName/_deletegroup")(managersOnly {
defining(params("groupName")){ groupName =>
// Remove from GROUP_MEMBER
updateGroupMembers(groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
}
redirect("/")
})
post("/:groupName/_editgroup", editGroupForm)(managersOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, false)
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect(s"/${form.groupName}")
} getOrElse NotFound
}
})
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { member =>
addCollaborator(form.owner, form.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
// redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
private def uniqueRepository: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) 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,11 +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 scala.Some import service.{SystemSettingsService, AccountService}
import service.AccountService import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import javax.servlet.http.{HttpServletResponse, HttpSession, 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._
@@ -21,14 +19,15 @@ import org.scalatra.i18n._
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase extends ScalatraFilter abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations { with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
// Don't set content type via Accept header. // Don't set content type via Accept header.
override def format(implicit request: HttpServletRequest) = "" override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
val httpRequest = request.asInstanceOf[HttpServletRequest] val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse] val httpResponse = response.asInstanceOf[HttpServletResponse]
val context = request.getServletContext.getContextPath val context = request.getServletContext.getContextPath
@@ -36,15 +35,16 @@ abstract class ControllerBase extends ScalatraFilter
if(path.startsWith("/console/")){ if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val baseUrl = this.baseUrl(httpRequest)
if(account == null){ if(account == null){
// Redirect to login form // Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path)) httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
} else if(account.isAdmin){ } else if(account.isAdmin){
// H2 Console (administrators only) // H2 Console (administrators only)
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
// Redirect to dashboard // Redirect to dashboard
httpResponse.sendRedirect(context + "/") httpResponse.sendRedirect(baseUrl + "/")
} }
} else if(path.startsWith("/git/")){ } else if(path.startsWith("/git/")){
// Git repository // Git repository
@@ -53,15 +53,24 @@ abstract class ControllerBase extends ScalatraFilter
// Scalatra actions // Scalatra actions
super.doFilter(request, response, chain) super.doFilter(request, response, chain)
} }
} finally {
contextCache.remove();
} }
private val contextCache = new java.lang.ThreadLocal[Context]()
/** /**
* Returns the context object for the request. * Returns the context object for the request.
*/ */
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request) implicit def context: Context = {
contextCache.get match {
private def currentURL: String = defining(request.getQueryString){ queryString => case null => {
request.getRequestURI + (if(queryString != null) "?" + queryString else "") val context = Context(loadSystemSettings(), LoginAccount, request)
contextCache.set(context)
context
}
case context => context
}
} }
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
@@ -107,27 +116,32 @@ abstract class ControllerBase extends ScalatraFilter
if(request.getMethod.toUpperCase == "POST"){ if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin")) org.scalatra.Unauthorized(redirect("/signin"))
} else { } else {
org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(currentURL))) org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
defining(request.getQueryString){ queryString =>
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
}
)))
} }
} }
} }
protected def baseUrl = defining(request.getRequestURL.toString){ url => override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) includeContextPath: Boolean = true, includeServletPath: Boolean = true)
} (implicit request: HttpServletRequest, response: HttpServletResponse) =
if (path.startsWith("http")) path
else baseUrl + url(path, params, false, false, false)
} }
/** /**
* Context object for the current request. * Context object for the current request.
*/ */
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
def redirectUrl = if(request.getParameter("redirect") != null){ val path = settings.baseUrl.getOrElse(request.getContextPath)
request.getParameter("redirect") val currentPath = request.getRequestURI.substring(request.getContextPath.length)
} else { val baseUrl = settings.baseUrl(request)
currentUrl val host = new java.net.URL(baseUrl).getHost
}
/** /**
* Get object from cache. * Get object from cache.
@@ -149,7 +163,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
/** /**
* 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 =
@@ -160,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))
@@ -182,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,191 +0,0 @@
package app
import util.Directory._
import util.ControlUtil._
import util._
import service._
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import org.scalatra.i18n.Messages
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator =>
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
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/%s".format(loginUserName, repository.name))
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
/**
* Duplicate check for the repository name.
*/
private def unique: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}

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

View File

@@ -1,368 +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.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys} 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
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService class IssuesController extends IssuesControllerBase
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService trait IssuesControllerBase extends ControllerBase {
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) case class IssueCreateForm(title: String, content: Option[String],
case class IssueEditForm(title: String, content: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class CommentForm(issueId: Int, content: String) case class IssueEditForm(title: String, content: Option[String])
case class IssueStateForm(issueId: Int, content: Option[String]) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), val issueCreateForm = mapping(
"content" -> trim(optional(text())), "title" -> trim(label("Title", text(required))),
"assignedUserName" -> trim(optional(text())), "content" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())), "assignedUserName" -> trim(optional(text())),
"labelNames" -> trim(optional(text())) "milestoneId" -> trim(optional(number())),
)(IssueCreateForm.apply) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply)
val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))), val issueEditForm = mapping(
"content" -> trim(optional(text())) "title" -> trim(label("Title", text(required))),
)(IssueEditForm.apply) "content" -> trim(optional(text()))
)(IssueEditForm.apply)
val commentForm = mapping(
"issueId" -> label("Issue Id", number()), val commentForm = mapping(
"content" -> trim(label("Comment", text(required))) "issueId" -> label("Issue Id", number()),
)(CommentForm.apply) "content" -> trim(label("Comment", text(required)))
)(CommentForm.apply)
val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()), val issueStateForm = mapping(
"content" -> trim(optional(text())) "issueId" -> label("Issue Id", number()),
)(IssueStateForm.apply) "content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _) get("/:owner/:repository/issues")(referrersOnly {
}) searchIssues("all", _)
})
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _) get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
}) searchIssues("assigned", _)
})
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _) get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
}) searchIssues("created_by", _)
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
getIssue(owner, name, issueId) map { defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
issues.html.issue( getIssue(owner, name, issueId) map {
_, issues.html.issue(
getComments(owner, name, issueId.toInt), _,
getIssueLabels(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, getIssueLabels(owner, name, issueId.toInt),
getMilestonesWithIssueCount(owner, name), (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getLabels(owner, name), getMilestonesWithIssueCount(owner, name),
hasWritePermission(owner, name, context.loginAccount), getLabels(owner, name),
repository) hasWritePermission(owner, name, context.loginAccount),
} getOrElse NotFound repository)
} } getOrElse NotFound
}) }
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
issues.html.create( defining(repository.owner, repository.name){ case (owner, name) =>
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, issues.html.create(
getMilestones(owner, name), (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getLabels(owner, name), getMilestones(owner, name),
hasWritePermission(owner, name, context.loginAccount), getLabels(owner, name),
repository) hasWritePermission(owner, name, context.loginAccount),
} repository)
}) }
})
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
val writable = hasWritePermission(owner, name, context.loginAccount) defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName
// insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content, // insert issue
if(writable) form.assignedUserName else None, val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.milestoneId else None) if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None)
// insert labels
if(writable){ // insert labels
form.labelNames.map { value => if(writable){
val labels = getLabels(owner, name) form.labelNames.map { value =>
value.split(",").foreach { labelName => val labels = getLabels(owner, name)
labels.find(_.labelName == labelName).map { label => value.split(",").foreach { labelName =>
registerIssueLabel(owner, name, issueId, label.labelId) labels.find(_.labelName == labelName).map { label =>
} registerIssueLabel(owner, name, issueId, label.labelId)
} }
} }
} }
}
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title) // record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ // extract references and create refer comment
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}") getIssue(owner, name, issueId.toString).foreach { issue =>
} createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
}
redirect(s"/${owner}/${name}/issues/${issueId}")
} // notifications
}) Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => }
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => redirect(s"/${owner}/${name}/issues/${issueId}")
if(isEditable(owner, name, issue.openedUserName)){ }
updateIssue(owner, name, issue.issueId, form.title, form.content) })
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
} getOrElse NotFound defining(repository.owner, repository.name){ case (owner, name) =>
} getIssue(owner, name, params("id")).map { issue =>
}) if(isEditable(owner, name, issue.openedUserName)){
// update issue
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => updateIssue(owner, name, issue.issueId, form.title, form.content)
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => // extract references and create refer comment
redirect(s"/${repository.owner}/${repository.name}/${ createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
}) } else Unauthorized
} getOrElse NotFound
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => }
handleComment(form.issueId, form.content, repository)() map { case (issue, id) => })
redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
} getOrElse NotFound handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
}) redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => } getOrElse NotFound
defining(repository.owner, repository.name){ case (owner, name) => })
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
updateComment(comment.commentId, form.content) handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") redirect(s"/${repository.owner}/${repository.name}/${
} else Unauthorized 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/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)){ updateComment(comment.commentId, form.content)
Ok(deleteComment(comment.commentId)) redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
} }
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { x => defining(repository.owner, repository.name){ case (owner, name) =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ getComment(owner, name, params("id")).map { comment =>
params.get("dataType") collect { if(isEditable(owner, name, comment.commentedUserName)){
case t if t == "html" => issues.html.editissue( Ok(deleteComment(comment.commentId))
x.title, x.content, x.issueId, x.userName, x.repositoryName) } else Unauthorized
} getOrElse { } getOrElse NotFound
contentType = formats("json") }
org.json4s.jackson.Serialization.write( })
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
repository, false, true) getIssue(repository.owner, repository.name, params("id")) map { x =>
)) if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
} params.get("dataType") collect {
} else Unauthorized case t if t == "html" => issues.html.editissue(
} getOrElse NotFound x.title, x.content, x.issueId, x.userName, x.repositoryName)
}) } getOrElse {
contentType = formats("json")
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => org.json4s.jackson.Serialization.write(
getComment(repository.owner, repository.name, params("id")) map { x => Map("title" -> x.title,
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
params.get("dataType") collect { repository, false, true)
case t if t == "html" => issues.html.editcomment( ))
x.content, x.commentId, x.userName, x.repositoryName) }
} getOrElse { } else Unauthorized
contentType = formats("json") } getOrElse NotFound
org.json4s.jackson.Serialization.write( })
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true) ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
)) getComment(repository.owner, repository.name, params("id")) map { x =>
} if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
} else Unauthorized params.get("dataType") collect {
} getOrElse NotFound case t if t == "html" => issues.html.editcomment(
}) x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse {
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => contentType = formats("json")
defining(params("id").toInt){ issueId => org.json4s.jackson.Serialization.write(
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) Map("content" -> view.Markdown.toHtml(x.content,
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) repository, false, true)
} ))
}) }
} else Unauthorized
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => } getOrElse NotFound
defining(params("id").toInt){ issueId => })
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
} defining(params("id").toInt){ issueId =>
}) registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => }
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) })
Ok("updated")
}) ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId =>
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
milestoneId("milestoneId").map { milestoneId => }
getMilestonesWithIssueCount(repository.owner, repository.name) })
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false) ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
} getOrElse NotFound updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
} getOrElse Ok() Ok("updated")
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
defining(params.get("value")){ action => updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
executeBatch(repository) { milestoneId("milestoneId").map { milestoneId =>
handleComment(_, None, repository)( _ => action) getMilestonesWithIssueCount(repository.owner, repository.name)
} .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
} issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}) } getOrElse NotFound
} getOrElse Ok()
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => })
params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { issueId => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { defining(params.get("value")){ action =>
registerIssueLabel(repository.owner, repository.name, issueId, labelId) executeBatch(repository) {
} handleComment(_, None, repository)( _ => action)
} }
} getOrElse NotFound }
}) })
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
defining(assignedUserName("value")){ value => params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { executeBatch(repository) { issueId =>
updateAssignedUserName(repository.owner, repository.name, _, value) getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
} registerIssueLabel(repository.owner, repository.name, issueId, labelId)
} }
}) }
} getOrElse NotFound
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => })
defining(milestoneId("value")){ value =>
executeBatch(repository) { post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, _, value) defining(assignedUserName("value")){ value =>
} executeBatch(repository) {
} updateAssignedUserName(repository.owner, repository.name, _, value)
}) }
}
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") })
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = defining(milestoneId("value")){ value =>
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value)
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { }
params("checked").split(',') map(_.toInt) foreach execute }
redirect(s"/${repository.owner}/${repository.name}/issues") })
}
get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
/** (Directory.getAttachedDir(repository.owner, repository.name) match {
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] case dir if(dir.exists && dir.isDirectory) =>
*/ dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) contentType = FileUtil.getMimeType(file.getName)
(getAction: model.Issue => Option[String] = file
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { }
case _ => None
defining(repository.owner, repository.name){ case (owner, name) => }) getOrElse NotFound
val userName = context.loginAccount.get.userName })
getIssue(owner, name, issueId.toString) map { issue => val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val (action, recordActivity) = val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
getAction(issue)
.collect { private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
case "close" => true -> (Some("close") -> hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
Some(recordReopenIssueActivity _)) params("checked").split(',') map(_.toInt) foreach execute
} redirect(s"/${repository.owner}/${repository.name}/issues")
.map { case (closed, t) => }
updateClosed(owner, name, issueId, closed)
t private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
} StringUtil.extractIssueId(message).foreach { issueId =>
.getOrElse(None -> None) if(getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
val commentId = content fromIssue.issueId + ":" + fromIssue.title, "refer")
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) }
.getOrElse ( action.get.capitalize -> action.get ) }
match { }
case (content, action) => createComment(owner, name, userName, issueId, content, action)
} /**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
// record activity */
content foreach { private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) (getAction: model.Issue => Option[String] =
(owner, name, userName, issueId, _) p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName
// notifications
Notifier() match { getIssue(owner, name, issueId.toString) map { issue =>
case f => val (action, recordActivity) =
content foreach { getAction(issue)
f.toNotify(repository, issueId, _){ .collect {
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${ case "close" => true -> (Some("close") ->
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
} case "reopen" => false -> (Some("reopen") ->
} Some(recordReopenIssueActivity _))
action foreach { }
f.toNotify(repository, issueId, _){ .map { case (closed, t) =>
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}") updateClosed(owner, name, issueId, closed)
} t
} }
} .getOrElse(None -> None)
issue -> commentId val commentId = content
} .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
} .getOrElse ( action.get.capitalize -> action.get )
} match {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { }
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", "")) // record activity
val page = IssueSearchCondition.page(request) content foreach {
val sessionKey = Keys.Session.Issues(owner, repoName) (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _)
// retrieve search condition }
val condition = session.putAndGet(sessionKey, recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) // extract references and create refer comment
) content.map { content =>
createReferComment(owner, name, issue, content)
issues.html.list( }
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, // notifications
(getCollaborators(owner, repoName) :+ owner).sorted, Notifier() match {
getMilestones(owner, repoName), case f =>
getLabels(owner, repoName), content foreach {
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), f.toNotify(repository, issueId, _){
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
countIssue(condition, Map.empty, false, owner -> repoName), if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
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), action foreach {
condition, f.toNotify(repository, issueId, _){
filter, Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
repository, }
hasWritePermission(owner, repoName, context.loginAccount)) }
} }
}
issue -> commentId
} }
}
}
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
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,31 +13,32 @@ 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
import org.eclipse.jgit.errors.NoMergeBaseException import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
class PullRequestsController extends PullRequestsControllerBase class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with ReferrerAuthenticator with CollaboratorsAuthenticator with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with ReferrerAuthenticator with CollaboratorsAuthenticator => with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping( val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))), "title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))), "content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))), "targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))), "targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))), "requestUserName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))), "requestRepositoryName" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))), "requestBranch" -> trim(text(required, maxlength(100))),
"commitIdTo" -> trim(text(required, maxlength(40))) "commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply) )(PullRequestForm.apply)
val mergeForm = mapping( val mergeForm = mapping(
@@ -50,6 +51,7 @@ trait PullRequestsControllerBase extends ControllerBase {
targetUserName: String, targetUserName: String,
targetBranch: String, targetBranch: String,
requestUserName: String, requestUserName: String,
requestRepositoryName: String,
requestBranch: String, requestBranch: String,
commitIdFrom: String, commitIdFrom: String,
commitIdTo: String) commitIdTo: String)
@@ -76,8 +78,10 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq( pulls.html.pullreq(
issue, pullreq, issue, pullreq,
getComments(owner, name, issueId), getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId),
(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),
commits, commits,
diffs, diffs,
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
@@ -90,21 +94,36 @@ trait PullRequestsControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
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
getPullRequest(owner, name, issueId) map { case(issue, pullreq) => getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
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
}) })
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
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.
@@ -157,15 +176,31 @@ 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 => // close issue by content of pull request
if(!existsCommitId(owner, name, commit.id)){ val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
insertCommitId(owner, name, commit.id) if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
} }
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(owner)){
callWebHook(owner, name, webHookURLs,
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
}
case _ =>
} }
// 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}")
@@ -178,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))
@@ -186,89 +221,101 @@ trait PullRequestsControllerBase extends ControllerBase {
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2 val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
} }
} getOrElse NotFound } getOrElse NotFound
} }
case _ => { case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) => JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
} getOrElse { } getOrElse {
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}") redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
} }
} }
} }
} }
}) })
get("/:owner/:repository/compare/*...*")(referrersOnly { repository => get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(getRepository(originOwner, repository.name, baseUrl), (for(
getRepository(forkedOwner, repository.name, baseUrl)) match { originRepositoryName <- if(originOwner == forkedOwner){
case (Some(originRepository), Some(forkedRepository)) => { Some(forkedRepository.name)
using( } else {
Git.open(getRepositoryDir(originOwner, repository.name)), forkedRepository.repository.originRepositoryName.orElse {
Git.open(getRepositoryDir(forkedOwner, repository.name)) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit,
originOwner, repository.name, originBranch,
forkedOwner, repository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originOwner, repository.name, oldId.getName,
forkedOwner, repository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
repository.repository.originUserName.map { userName =>
userName :: getForkedRepositories(userName, repository.name)
} getOrElse List(repository.owner),
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
repository,
originRepository,
forkedRepository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.name, oldId.getName,
forkedRepository.owner, forkedRepository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
forkedRepository,
originRepository,
forkedRepository,
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
} }
case _ => NotFound }) getOrElse NotFound
}
}) })
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(getRepository(originOwner, repository.name, baseUrl), (for(
getRepository(forkedOwner, repository.name, baseUrl)) match { originRepositoryName <- if(originOwner == forkedOwner){
case (Some(originRepository), Some(forkedRepository)) => { Some(forkedRepository.name)
using( } else {
Git.open(getRepositoryDir(originOwner, repository.name)), forkedRepository.repository.originRepositoryName.orElse {
Git.open(getRepositoryDir(forkedOwner, repository.name)) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
} }
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch))
} }
case _ => NotFound() }) getOrElse NotFound
}
}) })
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
@@ -290,7 +337,7 @@ trait PullRequestsControllerBase extends ControllerBase {
issueId = issueId, issueId = issueId,
originBranch = form.targetBranch, originBranch = form.targetBranch,
requestUserName = form.requestUserName, requestUserName = form.requestUserName,
requestRepositoryName = repository.name, requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch, requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom, commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo) commitIdTo = form.commitIdTo)
@@ -298,7 +345,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// fetch requested branch // fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch git.fetch
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString) .setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head")) .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call .call
} }
@@ -308,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}")
@@ -319,16 +366,16 @@ 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}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
withTmpRefSpec(new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true), git) { ref => try {
// fetch objects from origin repository branch // fetch objects from origin repository branch
git.fetch git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString) .setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
.setRefSpecs(ref) .setRefSpecs(refSpec)
.call .call
// merge conflict check // merge conflict check
@@ -340,6 +387,10 @@ trait PullRequestsControllerBase extends ControllerBase {
} catch { } catch {
case e: NoMergeBaseException => true case e: NoMergeBaseException => true
} }
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
refUpdate.setForceUpdate(true)
refUpdate.delete()
} }
} }
} }
@@ -351,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)
@@ -380,25 +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))
@@ -408,7 +442,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit) new CommitInfo(revCommit)
}.toList.splitWith{ (commit1, commit2) => }.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
} }
@@ -416,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) =>
@@ -432,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,202 +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.FlashMapSupport import util.JGitUtil.CommitInfo
import org.scalatra.i18n.Messages import jp.sf.amateras.scalatra.forms._
import service.WebHookService.WebHookPayload import org.apache.commons.io.FileUtils
import util.JGitUtil.CommitInfo import org.scalatra.i18n.Messages
import util.ControlUtil._ import service.WebHookService.WebHookPayload
import org.eclipse.jgit.api.Git import util.JGitUtil.CommitInfo
import util.ControlUtil._
class RepositorySettingsController extends RepositorySettingsControllerBase import org.eclipse.jgit.api.Git
with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport { with OwnerAuthenticator with UsersAuthenticator
self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator => trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService
// for repository options with OwnerAuthenticator with UsersAuthenticator =>
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
// for repository options
val optionsForm = mapping( 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(
/** "url" -> trim(label("url", text(required, webHook)))
* Redirect to the Options page. )(WebHookForm.apply)
*/
get("/:owner/:repository/settings")(ownerOnly { repository => // for transfer ownership
redirect(s"/${repository.owner}/${repository.name}/settings/options") case class TransferOwnerShipForm(newOwner: String)
})
val transferForm = mapping(
/** "newOwner" -> trim(label("New owner", text(required, transferUser)))
* Display the Options page. )(TransferOwnerShipForm.apply)
*/
get("/:owner/:repository/settings/options")(ownerOnly { /**
settings.html.options(_, flash.get("info")) * Redirect to the Options page.
}) */
get("/:owner/:repository/settings")(ownerOnly { repository =>
/** redirect(s"/${repository.owner}/${repository.name}/settings/options")
* Save the repository options. })
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => /**
saveRepositoryOptions( * Display the Options page.
repository.owner, */
repository.name, get("/:owner/:repository/settings/options")(ownerOnly {
form.description, settings.html.options(_, flash.get("info"))
form.defaultBranch, })
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate /**
} getOrElse form.isPrivate * Save the repository options.
) */
flash += "info" -> "Repository settings has been updated." post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
redirect(s"/${repository.owner}/${repository.name}/settings/options") saveRepositoryOptions(
}) repository.owner,
repository.name,
/** form.description,
* Display the Collaborators page. if(repository.branchList.isEmpty) "master" else form.defaultBranch,
*/ repository.repository.parentUserName.map { _ =>
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => repository.repository.isPrivate
settings.html.collaborators( } getOrElse form.isPrivate
getCollaborators(repository.owner, repository.name), )
getAccountByUserName(repository.owner).get.isGroupAccount, // Change repository name
repository) if(repository.name != form.repositoryName){
}) // Update database
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
/** // Move git repository
* Add the collaborator. defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
*/ FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => }
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ // Move wiki repository
addCollaborator(repository.owner, repository.name, form.userName) defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
} FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") }
}) }
flash += "info" -> "Repository settings has been updated."
/** redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
* Add the collaborator. })
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => /**
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ * Display the Collaborators page.
removeCollaborator(repository.owner, repository.name, params("name")) */
} get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") settings.html.collaborators(
}) getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
/** repository)
* 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 collaborator.
}) */
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
/** if(!getAccountByUserName(repository.owner).get.isGroupAccount){
* Add the web hook URL. addCollaborator(repository.owner, repository.name, form.userName)
*/ }
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
addWebHookURL(repository.owner, repository.name, form.url) })
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) /**
* Add the collaborator.
/** */
* Delete the web hook URL. get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
*/ if(!getAccountByUserName(repository.owner).get.isGroupAccount){
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => removeCollaborator(repository.owner, repository.name, params("name"))
deleteWebHookURL(repository.owner, repository.name, params("url")) }
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
/** /**
* Send the test request to registered web hook URLs. * Display the web hook page.
*/ */
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
import scala.collection.JavaConverters._ })
val commits = git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch)) /**
.setMaxCount(3) * Add the web hook URL.
.call.iterator.asScala.map(new CommitInfo(_)) */
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
val webHookURLs = getWebHookURLs(repository.owner, repository.name) addWebHookURL(repository.owner, repository.name, form.url)
if(webHookURLs.nonEmpty){ redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
val owner = getAccountByUserName(repository.owner).get })
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload( /**
git, * Delete the web hook URL.
owner, */
"refs/heads/" + repository.repository.defaultBranch, get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
repository, deleteWebHookURL(repository.owner, repository.name, params("url"))
commits.toList, redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
owner)) })
}
/**
flash += "info" -> "Test payload deployed!" * Send the test request to registered web hook URLs.
} */
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
}) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
/** val commits = git.log
* Display the delete repository page. .add(git.getRepository.resolve(repository.repository.defaultBranch))
*/ .setMaxCount(3)
get("/:owner/:repository/settings/delete")(ownerOnly { .call.iterator.asScala.map(new CommitInfo(_))
settings.html.delete(_)
}) getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
/** for(ownerAccount <- getAccountByUserName(repository.owner)){
* Delete the repository. callWebHook(repository.owner, repository.name, webHookURLs,
*/ WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
post("/:owner/:repository/settings/delete")(ownerOnly { repository => }
deleteRepository(repository.owner, repository.name) case _ =>
}
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) flash += "info" -> "Test payload deployed!"
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) }
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
redirect(s"/${repository.owner}") })
})
/**
/** * Display the danger zone.
* Provides duplication check for web hook url. */
*/ get("/:owner/:repository/settings/danger")(ownerOnly {
private def webHook: Constraint = new Constraint(){ settings.html.danger(_)
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.")
} /**
* Transfer repository ownership.
/** */
* Provides Constraint to validate the collaborator name. post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
*/ // Change repository owner
private def collaborator: Constraint = new Constraint(){ if(repository.owner != form.newOwner){
override def validate(name: String, value: String, messages: Messages): Option[String] = LockUtil.lock(s"${repository.owner}/${repository.name}"){
getAccountByUserName(value) match { // Update database
case None => Some("User does not exist.") renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
case Some(x) if(x.isGroupAccount) // Move git repository
=> Some("User does not exist.") defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
=> Some("User can access this repository already.") }
case _ => None // Move wiki repository
} defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
} FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
}
}
}
redirect(s"/${form.newOwner}/${repository.name}")
})
/**
* Delete the repository.
*/
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}"){
deleteRepository(repository.owner, repository.name)
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
}
redirect(s"/${repository.owner}")
})
/**
* Provides duplication check for web hook url.
*/
private def webHook: Constraint = new Constraint(){
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 Constraint to validate the collaborator name.
*/
private def collaborator: 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.isGroupAccount)
=> Some("User does not exist.")
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.")
case _ => None
}
}
/**
* 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 =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}
/**
* Provides Constraint to validate the repository transfer user.
*/
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")){
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,9 +1,10 @@
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.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil} import _root_.util._
import service._ import service._
import org.scalatra._ import org.scalatra._
import java.io.File import java.io.File
@@ -12,15 +13,52 @@ 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 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 ReferrerAuthenticator 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 ReferrerAuthenticator => 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.
@@ -70,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.
*/ */
@@ -79,46 +179,18 @@ 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 if(raw){
def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match { // Download
case true if(walk.getPathString == path) => walk.getObjectId(0) defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
case true => getPathObjectId(path, walk) contentType = FileUtil.getContentType(path, bytes)
} bytes
val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
}
if(raw){
// Download
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
// 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.getContent(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 { } else {
// image or large repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
JGitUtil.ContentInfo(viewer, None) new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
} getOrElse NotFound
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
}
} }
}) })
@@ -150,10 +222,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen) (branchName, revCommit.getCommitterIdent.getWhen)
} }
repo.html.branches(branchInfo, repository) repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
} }
}) })
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
redirect(s"/${repository.owner}/${repository.name}/branches")
})
/** /**
* Displays tags. * Displays tags.
*/ */
@@ -164,18 +251,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/** /**
* Download repository contents as an archive. * Download repository contents as an archive.
*/ */
get("/:owner/:repository/archive/:name")(referrersOnly { repository => get("/:owner/:repository/archive/*")(referrersOnly { repository =>
val name = params("name") 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)
} }
workDir.mkdirs workDir.mkdirs
val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip") val zipFile = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + ".zip")
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(revision)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
@@ -190,7 +278,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
while(walk.next){ while(walk.next){
val name = walk.getPathString val name = walk.getPathString
val mode = walk.getFileMode(0) val mode = walk.getFileMode(0)
if(mode != FileMode.TREE){ if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
walk.getObjectId(objectId, 0) walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name) val entry = new ZipEntry(name)
val loader = reader.open(objectId) val loader = reader.open(objectId)
@@ -204,6 +292,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
contentType = "application/octet-stream" contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile zipFile
} else { } else {
BadRequest BadRequest
@@ -215,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)
@@ -245,30 +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 =>
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) val path = (file.name :: parentPath.reverse).reverse
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
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,17 +2,15 @@ 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._
class SearchController extends SearchControllerBase class SearchController extends SearchControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
with RepositorySearchService with IssuesService
with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService trait SearchControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService with RepositorySearchService with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
with ReferrerAuthenticator =>
val searchForm = mapping( val searchForm = mapping(
"query" -> trim(text(required)), "query" -> trim(text(required)),

View File

@@ -1,58 +0,0 @@
package app
import service._
import jp.sf.amateras.scalatra.forms._
import util.Implicits._
import util.StringUtil._
import util.Keys
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
trait SignInControllerBase extends ControllerBase { self: SystemSettingsService with AccountService =>
case class SignInForm(userName: String, password: String)
val form = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
get("/signin"){
val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute(Keys.Session.Redirect, redirect.get)
}
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
authenticate(loadSystemSettings(), form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
}
}
get("/signout"){
session.invalidate
redirect("/")
}
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: model.Account) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
}
}.getOrElse {
redirect("/")
}
}
}

View File

@@ -3,19 +3,27 @@ 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 org.scalatra.FlashMapSupport 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 SystemSettingsService with AccountService with AdminAuthenticator with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { trait SystemSettingsControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with AdminAuthenticator => self: AccountService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping( "smtp" -> optionalIfNotChecked("notification", mapping(
"host" -> trim(label("SMTP Host", text(required))), "host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))), "port" -> trim(label("SMTP Port", optional(number()))),
@@ -33,21 +41,144 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
"bindPassword" -> trim(label("Bind Password", optional(text()))), "bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))), "baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))), "userNameAttribute" -> trim(label("User name attribute", text(required))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", text(required))), "mailAttribute" -> trim(label("Mail address attribute", text(required))),
"tls" -> trim(label("Enable TLS", optional(boolean()))), "tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text()))) "keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply)) )(Ldap.apply))
)(SystemSettings.apply) )(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} 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(loadSystemSettings(), flash.get("info")) admin.html.system(flash.get("info"))
}) })
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){
SshServer.start(request.getServletContext,
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop()
}
flash += "info" -> "System settings has been updated." flash += "info" -> "System settings has been updated."
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,9 +4,11 @@ import service._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import util.Directory._ import util.Directory._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils
class UserManagementController extends UserManagementControllerBase class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
@@ -23,10 +25,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String]) members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean) members: String, clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping( val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
@@ -51,28 +53,28 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text()))) "members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply) )(NewGroupForm.apply)
val editGroupForm = mapping( val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text()))), "members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean())), "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean())) "removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply) )(EditGroupForm.apply)
get("/admin/users")(adminOnly { get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved) val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
val members = users.collect { case account if(account.isGroupAccount) => account.userName -> getGroupMembers(account.userName).map(_.userName)
account.userName -> getGroupMembers(account.userName)
}.toMap }.toMap
admin.users.html.list(users, members, includeRemoved) admin.users.html.list(users, members, includeRemoved)
}) })
@@ -127,7 +129,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url) createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil)) updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false) updateImage(form.groupName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
@@ -139,7 +145,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}) })
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) => defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account => getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved) updateGroup(groupName, form.url, form.isRemoved)
@@ -155,11 +165,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
} else { } else {
// Update GROUP_MEMBER // Update GROUP_MEMBER
updateGroupMembers(form.groupName, memberNames) updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories // Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName) removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName => members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName) addCollaborator(form.groupName, repositoryName, userName)
} }
} }
@@ -172,8 +182,17 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
}) })
post("/admin/users/_usercheck")(adminOnly { // TODO Move to other generic controller?
post("/admin/users/_usercheck"){
getAccountByUserName(params("userName")).isDefined getAccountByUserName(params("userName")).isDefined
}) }
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
} }

View File

@@ -4,20 +4,17 @@ 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.FlashMapSupport
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
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase with FlashMapSupport { trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
@@ -39,7 +36,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
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")
}) })
@@ -47,7 +45,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
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")
}) })
@@ -188,16 +187,16 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
private def conflictForNew: Constraint = new Constraint(){ private def conflictForNew: 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] = {
optionIf(targetWikiPage.nonEmpty){ targetWikiPage.map { _ =>
Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.") "Someone has created the wiki since you started. Please reload this page and re-apply your changes."
} }
} }
} }
private def conflictForEdit: Constraint = new Constraint(){ private def conflictForEdit: 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] = {
optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(false)){ targetWikiPage.filter(_.id != params("id")).map{ _ =>
Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.") "Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
} }
} }
} }

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,14 +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 * = groupName ~ userName <> (GroupMember, GroupMember.unapply _) val groupName = column[String]("GROUP_NAME", O PrimaryKey)
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
)

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

@@ -0,0 +1,24 @@
package model
trait SshKeyComponent { self: Profile =>
import profile.simple._
lazy val SshKeys = TableQuery[SshKeys]
class SshKeys(tag: Tag) extends Table[SshKey](tag, "SSH_KEY") {
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)
}
case class SshKey(
userName: String,
sshKeyId: Int = 0,
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,15 +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(mailAddress) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(userName) match { getAccountByUserName(ldapUserInfo.userName, true) match {
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress)) case Some(x) if(!x.isRemoved) => {
case None => createAccount(userName, "", userName, mailAddress, false, None) updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, 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 => 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}")
@@ -51,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 is mailAddress.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,
@@ -79,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,
@@ -94,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 = "",
@@ -115,36 +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]): 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 { userName => members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName) GroupMembers insert GroupMember (groupName, userName, isManager)
} }
} }
def getGroupMembers(groupName: String): List[String] = 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)
.map(_.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,373 +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
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.map { case (((t1, t2), t3), t4) => .sortBy { case (t1, t2) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) (condition.sort match {
} case "created" => t1.registeredDate
.sortBy(_._4) // labelName case "comments" => t2.commentCount
.sortBy { case (t1, commentCount, _,_,_) => case "updated" => t1.updatedDate
(condition.sort match { }) match {
case "created" => t1.registeredDate case sort => condition.direction match {
case "comments" => commentCount case "asc" => sort asc
case "updated" => t1.updatedDate case "desc" => sort desc
}) match { }
case sort => condition.direction match { }
case "asc" => sort asc }
case "desc" => sort desc .drop(offset).take(limit)
} .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
} .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
} .map { case (((t1, t2), t3), t4) =>
.drop(offset).take(limit) (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
.list }
.splitWith { (c1, c2) => .list
c1._1.userName == c2._1.userName && .splitWith { (c1, c2) =>
c1._1.repositoryName == c2._1.repositoryName && c1._1.userName == c2._1.userName &&
c1._1.issueId == c2._1.issueId c1._1.repositoryName == c2._1.repositoryName &&
} c1._1.issueId == c2._1.issueId
.map { issues => issues.head match { }
case (issue, commentCount, _,_,_) => .map { issues => issues.head match {
(issue, case (issue, commentCount, _,_,_) =>
issues.flatMap { t => t._3.map ( (issue,
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) issues.flatMap { t => t._3.map (
)} toList, Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
commentCount) )} toList,
}} toList commentCount)
} }} toList
}
/**
* Assembles query for conditional issue searching. /**
*/ * Assembles query for conditional issue searching.
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, */
filterUser: Map[String, String], onlyPullRequest: Boolean) = private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
Query(Issues) filter { t1 => filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
condition.repo Issues filter { t1 =>
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } condition.repo
.getOrElse (repos) .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.map { case (owner, repository) => t1.byRepository(owner, repository) } .getOrElse (repos)
.foldLeft[Column[Boolean]](false) ( _ || _ ) && .map { case (owner, repository) => t1.byRepository(owner, repository) }
(t1.closed is (condition.state == "closed").bind) && .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) && (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && (t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(t1.pullRequest is true.bind, onlyPullRequest) && (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(IssueLabels filter { t2 => (t1.pullRequest is true.bind, onlyPullRequest) &&
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (IssueLabels filter { t2 =>
(t2.labelId in (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(Labels filter { t3 => (t2.labelId in
(t3.byRepository(t1.userName, t1.repositoryName)) && (Labels filter { t3 =>
(t3.labelName inSetBind condition.labels) (t3.byRepository(t1.userName, t1.repositoryName)) &&
} map(_.labelId))) (t3.labelName inSetBind condition.labels)
} exists, condition.labels.nonEmpty) } 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) = def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
// next id number assignedUserName: Option[String], milestoneId: Option[Int],
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] isPullRequest: Boolean = false)(implicit s: Session) =
.firstOption.filter { id => // next id number
Issues insert Issue( sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
owner, .firstOption.filter { id =>
repository, Issues insert Issue(
id, owner,
loginUser, repository,
milestoneId, id,
assignedUserName, loginUser,
title, milestoneId,
content, assignedUserName,
false, title,
currentDate, content,
currentDate, false,
isPullRequest) currentDate,
currentDate,
// increment issue id isPullRequest)
IssueId
.filter (_.byPrimaryKey(owner, repository)) // increment issue id
.map (_.issueId) IssueId
.update (id) > 0 .filter (_.byPrimaryKey(owner, repository))
} get .map (_.issueId)
.update (id) > 0
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = } get
IssueLabels insert (IssueLabel(owner, repository, issueId, labelId))
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = IssueLabels insert IssueLabel(owner, repository, issueId, labelId)
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
def createComment(owner: String, repository: String, loginUser: String, IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
issueId: Int, content: String, action: String) =
IssueComments.autoInc insert ( def createComment(owner: String, repository: String, loginUser: String,
owner, issueId: Int, content: String, action: String)(implicit s: Session): Int =
repository, IssueComments.autoInc insert IssueComment(
issueId, userName = owner,
action, repositoryName = repository,
loginUser, issueId = issueId,
content, action = action,
currentDate, commentedUserName = loginUser,
currentDate) content = content,
registeredDate = currentDate,
def updateIssue(owner: String, repository: String, issueId: Int, updatedDate = currentDate)
title: String, content: Option[String]) =
Issues def updateIssue(owner: String, repository: String, issueId: Int,
.filter (_.byPrimaryKey(owner, repository, issueId)) title: String, content: Option[String])(implicit s: Session) =
.map { t => Issues
t.title ~ t.content.? ~ t.updatedDate .filter (_.byPrimaryKey(owner, repository, issueId))
} .map { t =>
.update (title, content, currentDate) (t.title, t.content.?, t.updatedDate)
}
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) = .update (title, content, currentDate)
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
def updateAssignedUserName(owner: String, repository: String, issueId: Int,
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) = assignedUserName: Option[String])(implicit s: Session) =
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
def updateComment(commentId: Int, content: String) = def updateMilestoneId(owner: String, repository: String, issueId: Int,
IssueComments milestoneId: Option[Int])(implicit s: Session) =
.filter (_.byPrimaryKey(commentId)) Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
.map { t =>
t.content ~ t.updatedDate def updateComment(commentId: Int, content: String)(implicit s: Session) =
} IssueComments
.update (content, currentDate) .filter (_.byPrimaryKey(commentId))
.map { t =>
def deleteComment(commentId: Int) = t.content -> t.updatedDate
IssueComments filter (_.byPrimaryKey(commentId)) delete }
.update (content, currentDate)
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
Issues def deleteComment(commentId: Int)(implicit s: Session) =
.filter (_.byPrimaryKey(owner, repository, issueId)) IssueComments filter (_.byPrimaryKey(commentId)) delete
.map { t =>
t.closed ~ t.updatedDate def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) =
} Issues
.update (closed, currentDate) .filter (_.byPrimaryKey(owner, repository, issueId))
.map { t =>
/** t.closed -> t.updatedDate
* Search issues by keyword. }
* .update (closed, currentDate)
* @param owner the repository owner
* @param repository the repository name /**
* @param query the keywords separated by whitespace. * Search issues by keyword.
* @return issues with comment count and matched content of issue or comment *
*/ * @param owner the repository owner
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = { * @param repository the repository name
import scala.slick.driver.H2Driver.likeEncode * @param query the keywords separated by whitespace.
val keywords = splitWords(query.toLowerCase) * @return issues with comment count and matched content of issue or comment
*/
// Search Issue def searchIssuesByKeyword(owner: String, repository: String, query: String)
val issues = Issues (implicit s: Session): List[(Issue, Int, String)] = {
.innerJoin(IssueOutline).on { case (t1, t2) => import slick.driver.JdbcDriver.likeEncode
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) val keywords = splitWords(query.toLowerCase)
}
.filter { case (t1, t2) => // Search Issue
keywords.map { keyword => val issues = Issues
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || .innerJoin(IssueOutline).on { case (t1, t2) =>
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
} .reduceLeft(_ && _) }
} .filter { case (t1, t2) =>
.map { case (t1, t2) => keywords.map { keyword =>
(t1, 0, t1.content.?, t2.commentCount) (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
} (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
} .reduceLeft(_ && _)
// Search IssueComment }
val comments = IssueComments .map { case (t1, t2) =>
.innerJoin(Issues).on { case (t1, t2) => (t1, 0, t1.content.?, t2.commentCount)
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
}
.innerJoin(IssueOutline).on { case ((t1, t2), t3) => // Search IssueComment
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) val comments = IssueComments
} .innerJoin(Issues).on { case (t1, t2) =>
.filter { case ((t1, t2), t3) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
keywords.map { query => }
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') .innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
}.reduceLeft(_ && _) t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
} }
.map { case ((t1, t2), t3) => .filter { case ((t1, t2), t3) =>
(t2, t1.commentId, t1.content.?, t3.commentCount) keywords.map { query =>
} t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
}.reduceLeft(_ && _)
issues.union(comments).sortBy { case (issue, commentId, _, _) => }
issue.issueId ~ commentId .map { case ((t1, t2), t3) =>
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => (t2, t1.commentId, t1.content.?, t3.commentCount)
issue1.issueId == issue2.issueId }
}.map { _.head match {
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) issues.union(comments).sortBy { case (issue, commentId, _, _) =>
} issue.issueId -> commentId
}.toList }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
} issue1.issueId == issue2.issueId
}.map { _.head match {
} case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
}
object IssuesService { }.toList
import javax.servlet.http.HttpServletRequest }
val IssueLimit = 30 def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = {
extractCloseId(message).foreach { issueId =>
case class IssueSearchCondition( for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
labels: Set[String] = Set.empty, createComment(owner, repository, userName, issue.issueId, "Close", "close")
milestoneId: Option[Option[Int]] = None, updateClosed(owner, repository, issue.issueId, true)
repo: Option[String] = None, }
state: String = "open", }
sort: String = "created", }
direction: String = "desc"){ }
def toURL: String = object IssuesService {
"?" + List( import javax.servlet.http.HttpServletRequest
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
milestoneId.map { id => "milestone=" + (id match { val IssueLimit = 30
case Some(x) => x.toString
case None => "none" case class IssueSearchCondition(
})}, labels: Set[String] = Set.empty,
repo.map("for=" + urlEncode(_)), milestoneId: Option[Option[Int]] = None,
Some("state=" + urlEncode(state)), repo: Option[String] = None,
Some("sort=" + urlEncode(sort)), state: String = "open",
Some("direction=" + urlEncode(direction))).flatten.mkString("&") sort: String = "created",
direction: String = "desc"){
}
def toURL: String =
object IssueSearchCondition { "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { milestoneId.map { id => "milestone=" + (id match {
val value = request.getParameter(name) case Some(x) => x.toString
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) case None => "none"
} })},
repo.map("for=" + urlEncode(_)),
def apply(request: HttpServletRequest): IssueSearchCondition = Some("state=" + urlEncode(state)),
IssueSearchCondition( Some("sort=" + urlEncode(sort)),
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty), Some("direction=" + urlEncode(direction))).flatten.mkString("&")
param(request, "milestone").map{
case "none" => None }
case x => x.toIntOpt
}, object IssueSearchCondition {
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"), private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), val value = request.getParameter(name)
param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
}
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt def apply(request: HttpServletRequest): IssueSearchCondition =
if(i <= 0) 1 else i IssueSearchCondition(
} catch { param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
case e: NumberFormatException => 1 param(request, "milestone").map{
} case "none" => None
} case x => x.toIntOpt
},
} param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
}
}

View File

@@ -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,11 +59,12 @@ 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()) {
if(treeWalk.getFileMode(0) != FileMode.TREE){ val mode = treeWalk.getFileMode(0)
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes => if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){ if(FileUtil.isText(bytes)){
val text = StringUtil.convertFromByteArray(bytes) val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase val lowerText = text.toLowerCase

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,9 +39,74 @@ trait RepositoryService { self: AccountService =>
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
def deleteRepository(userName: String, repositoryName: String): Unit = { def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String)
(implicit s: Session): Unit = {
getAccountByUserName(newUserName).foreach { account =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t =>
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
}
// Update activity messages
val updateActivities = Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId is activityId.bind).map(_.message).update(
message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
)
}
}
}
}
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
@@ -60,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.
@@ -71,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
@@ -86,22 +151,29 @@ trait RepositoryService { self: AccountService =>
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
)) ),
getRepositoryManagers(repository.userName))
} }
} }
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),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
)) ),
getRepositoryManagers(repository.userName))
} }
} }
@@ -112,45 +184,61 @@ 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),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
)) ),
getRepositoryManagers(repository.userName))
} }
} }
private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else {
Seq(userName)
}
/** /**
* 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)
/** /**
@@ -160,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.
@@ -170,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
/** /**
@@ -179,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
/** /**
@@ -189,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
@@ -201,37 +289,42 @@ 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] = 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(_.userName).list .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
} }
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){ branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
/** /**
* Creates instance with issue count and pull request count. * Creates instance with issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags) this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
/** /**
* Creates instance without issue count and pull request count. * Creates instance without issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags) this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
} }
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])

View File

@@ -1,7 +1,8 @@
package service package service
import model._ import model._
import service.SystemSettingsService.SystemSettings 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.
@@ -9,29 +10,29 @@ import service.SystemSettingsService.SystemSettings
* It may be called many times in one request, so each method stores * It may be called many times in one request, so each method stores
* its result into the cache which available during a request. * its result into the cache which available during a request.
*/ */
trait RequestCache { trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
def getSystemSettings()(implicit context: app.Context): SystemSettings = private implicit def context2Session(implicit context: app.Context): JdbcBackend#Session =
context.cache("system_settings"){ request2Session(context.request)
new SystemSettingsService {}.loadSystemSettings()
}
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = { 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}"){
new IssuesService {}.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}"){
new AccountService {}.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}"){
new AccountService {}.getAccountByMailAddress(mailAddress) super.getAccountByMailAddress(mailAddress)
} }
} }
} }

View File

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

View File

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

View File

@@ -1,300 +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.{PatchUtil, Directory, JGitUtil, LockUtil} 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._
object WikiService {
object WikiService { /**
* The model for wiki page.
/** *
* The model for wiki page. * @param name the page name
* * @param content the page content
* @param name the page name * @param committer the last committer
* @param content the page content * @param time the last modified time
* @param committer the last committer * @param id the latest commit id
* @param time the last modified time */
* @param id the latest commit id case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
*/
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) /**
* The model for wiki page history.
/** *
* The model for wiki page history. * @param name the page name
* * @param committer the committer the committer
* @param name the page name * @param message the commit message
* @param committer the committer the committer * @param date the commit date
* @param message the commit message */
* @param date the commit date case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
*/
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
} def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
trait WikiService { }
import WikiService._
trait WikiService {
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = import WikiService._
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
if(!dir.exists){ LockUtil.lock(s"${owner}/${repository}/wiki"){
JGitUtil.initRepository(dir) defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) 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] = { * Returns the wiki page.
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => */
optionIf(!JGitUtil.isEmpty(git)){ def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId) if(!JGitUtil.isEmpty(git)){
} JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
} 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.
optionIf(!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)
}
} 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.replaceFirst("\\.md$", "")) using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
.sortBy(x => x) JGitUtil.getFileList(git, "master", ".")
} .filter(_.name.endsWith(".md"))
} .map(_.name.stripSuffix(".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.replaceFirst("\\.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.stripSuffix(".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()
using(new RevWalk(git.getRepository)){ revWalk => val inserter = git.getRepository.newObjectInserter()
using(new TreeWalk(git.getRepository)){ treeWalk => val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true) JGitUtil.processTree(git, headId){ (path, tree) =>
while(treeWalk.next){ if(revertInfo.find(x => x.filePath == path).isEmpty){
val path = treeWalk.getPathString builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) }
if(revertInfo.find(x => x.filePath == path).isEmpty){ }
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} 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"))))
} }
} builder.finish()
revertInfo.filter(_.operation == "ADD").foreach { x => JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8")))) pageName match {
} case Some(x) => s"Revert ${from} ... ${to} on ${x}"
builder.finish() case None => s"Revert ${from} ... ${to}"
})
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, }
pageName match { }
case Some(x) => s"Revert ${from} ... ${to} on ${x}" }
case None => s"Revert ${from} ... ${to}" true
}) } catch {
} case e: Exception => {
} e.printStackTrace()
} false
true }
} catch { }
case e: Exception => { }
e.printStackTrace()
false /**
} * Save the wiki page.
} */
} def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
/** LockUtil.lock(s"${owner}/${repository}/wiki"){
* Save the wiki page. using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
*/ val builder = DirCache.newInCore.builder()
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, val inserter = git.getRepository.newObjectInserter()
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = { val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
LockUtil.lock(s"${owner}/${repository}/wiki"){ var created = true
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => var updated = false
val builder = DirCache.newInCore.builder() var removed = false
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") if(headId != null){
var created = true JGitUtil.processTree(git, headId){ (path, tree) =>
var updated = false if(path == currentPageName + ".md" && currentPageName != newPageName){
var removed = false removed = true
} else if(path != newPageName + ".md"){
if(headId != null){ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
using(new RevWalk(git.getRepository)){ revWalk => } else {
using(new TreeWalk(git.getRepository)){ treeWalk => created = false
val index = treeWalk.addTree(revWalk.parseTree(headId)) updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
treeWalk.setRecursive(true) }
while(treeWalk.next){ }
val path = treeWalk.getPathString }
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path == currentPageName + ".md" && currentPageName != newPageName){ if(created || updated || removed){
removed = true builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
} else if(path != newPageName + ".md"){ builder.finish()
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
} else { if(message.trim.length == 0) {
created = false if(removed){
updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) s"Rename ${currentPageName} to ${newPageName}"
} } else if(created){
} s"Created ${newPageName}"
} } else {
} s"Updated ${newPageName}"
} }
} else {
optionIf(created || updated || removed){ message
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, Some(newHeadId.getName)
if(message.trim.length == 0) { } else None
if(removed){ }
s"Rename ${currentPageName} to ${newPageName}" }
} else if(created){ }
s"Created ${newPageName}"
} else { /**
s"Updated ${newPageName}" * Delete the wiki page.
} */
} else { def deleteWikiPage(owner: String, repository: String, pageName: String,
message committer: String, mailAddress: String, message: String): Unit = {
}) LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
Some(newHeadId) val builder = DirCache.newInCore.builder()
} val inserter = git.getRepository.newObjectInserter()
} val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
} var removed = false
}
JGitUtil.processTree(git, headId){ (path, tree) =>
/** if(path != pageName + ".md"){
* Delete the wiki page. builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
*/ } else {
def deleteWikiPage(owner: String, repository: String, pageName: String, removed = true
committer: String, mailAddress: String, message: String): Unit = { }
LockUtil.lock(s"${owner}/${repository}/wiki"){ }
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => if(removed){
val builder = DirCache.newInCore.builder() builder.finish()
val inserter = git.getRepository.newObjectInserter() JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
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,161 +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, 8), */
Version(1, 7), val versions = Seq(
Version(1, 6), new Version(2, 1),
Version(1, 5), new Version(2, 0){
Version(1, 4), override def update(conn: Connection): Unit = {
new Version(1, 3){ import eu.medsea.mimeutil.{MimeUtil2, MimeType}
override def update(conn: Connection): Unit = {
super.update(conn) val mimeUtil = new MimeUtil2()
// Fix wiki repository configuration mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){ super.update(conn)
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
defining(git.getRepository.getConfig){ config => while(rs.next){
if(!config.getBoolean("http", "receivepack", false)){ defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
config.setBoolean("http", null, "receivepack", true) if(dir.exists && dir.isDirectory){
config.save dir.listFiles.foreach { file =>
} if(file.getName.indexOf('.') < 0){
} val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
} if(mimeType.startsWith("image/")){
} file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
} }
} }
}, }
Version(1, 2), }
Version(1, 1), }
Version(1, 0), }
Version(0, 0) }
) }
},
/** Version(1, 13),
* The head version of BitBucket. Version(1, 12),
*/ Version(1, 11),
val headVersion = versions.head Version(1, 10),
Version(1, 9),
/** Version(1, 8),
* The version file (GITBUCKET_HOME/version). Version(1, 7),
*/ Version(1, 6),
lazy val versionFile = new File(GitBucketHome, "version") Version(1, 5),
Version(1, 4),
/** new Version(1, 3){
* Returns the current version from the version file. override def update(conn: Connection): Unit = {
*/ super.update(conn)
def getCurrentVersion(): Version = { // Fix wiki repository configuration
if(versionFile.exists){ using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match { while(rs.next){
case Array(majorVersion, minorVersion) => { using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
versions.find { v => defining(git.getRepository.getConfig){ config =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt if(!config.getBoolean("http", "receivepack", false)){
}.getOrElse(Version(0, 0)) config.setBoolean("http", null, "receivepack", true)
} config.save
case _ => Version(0, 0) }
} }
} else Version(0, 0) }
} }
}
} }
},
/** Version(1, 2),
* Update database schema automatically in the context initializing. Version(1, 1),
*/ Version(1, 0),
class AutoUpdateListener extends ServletContextListener { Version(0, 0)
import AutoUpdate._ )
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
/**
override def contextInitialized(event: ServletContextEvent): Unit = { * The head version of BitBucket.
val datadir = event.getServletContext.getInitParameter("gitbucket.home") */
if(datadir != null){ val headVersion = versions.head
System.setProperty("gitbucket.home", datadir)
} /**
org.h2.Driver.load() * The version file (GITBUCKET_HOME/version).
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}") */
lazy val versionFile = new File(GitBucketHome, "version")
logger.debug("Start schema update")
defining(getConnection(event.getServletContext)){ conn => /**
try { * Returns the current version from the version file.
defining(getCurrentVersion()){ currentVersion => */
if(currentVersion == headVersion){ def getCurrentVersion(): Version = {
logger.debug("No update") if(versionFile.exists){
} else if(!versions.contains(currentVersion)){ FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") case Array(majorVersion, minorVersion) => {
} else { versions.find { v =>
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") }.getOrElse(Version(0, 0))
conn.commit() }
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") case _ => Version(0, 0)
} }
} } else Version(0, 0)
} catch { }
case ex: Throwable => {
logger.error("Failed to schema update", ex) }
ex.printStackTrace()
conn.rollback() /**
} * Update database schema automatically in the context initializing.
} */
} class AutoUpdateListener extends ServletContextListener {
logger.debug("End schema update") import org.quartz.impl.StdSchedulerFactory
} import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
def contextDestroyed(sce: ServletContextEvent): Unit = { import org.quartz.SimpleScheduleBuilder._
// Nothing to do. import AutoUpdate._
}
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private def getConnection(servletContext: ServletContext): Connection = private val scheduler = StdSchedulerFactory.getDefaultScheduler
DriverManager.getConnection(
servletContext.getInitParameter("db.url"), override def contextInitialized(event: ServletContextEvent): Unit = {
servletContext.getInitParameter("db.user"), val datadir = event.getServletContext.getInitParameter("gitbucket.home")
servletContext.getInitParameter("db.password")) if(datadir != null){
System.setProperty("gitbucket.home", datadir)
} }
org.h2.Driver.load()
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

@@ -8,14 +8,16 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.ServletContext import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import util.{Keys, JGitUtil, Directory} import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._ import util.Implicits._
import service._ 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.
@@ -23,7 +25,7 @@ import util.JGitUtil.CommitInfo
* This servlet provides only Git repository functionality. * This servlet provides only Git repository functionality.
* Authentication is provided by [[servlet.BasicAuthenticationFilter]]. * Authentication is provided by [[servlet.BasicAuthenticationFilter]].
*/ */
class GitRepositoryServlet extends GitServlet { class GitRepositoryServlet extends GitServlet with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
@@ -47,29 +49,45 @@ class GitRepositoryServlet extends GitServlet {
super.init(config) super.init(config)
} }
override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = {
val agent = req.getHeader("USER-AGENT")
val index = req.getRequestURI.indexOf(".git")
if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){
// redirect for browsers
val paths = req.getRequestURI.substring(0, index).split("/")
res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last)
} else {
// response for git client
super.service(req, res)
}
}
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] { 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 = {
val receivePack = new ReceivePack(db) val receivePack = new ReceivePack(db)
val userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String] val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI) logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName) logger.debug("pusher:" + pusher)
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")
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL)) if(!repository.endsWith(".wiki")){
defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
}
receivePack receivePack
} }
} }
@@ -77,93 +95,119 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, userName: 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 onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val commits = command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
}
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
// Extract new commit and apply issue comment def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
val newCommits = if(commits.size > 1000){ try {
val existIds = getAllCommitIds(owner, repository) using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commits.flatMap { commit => existIds = JGitUtil.getAllCommitIds(git)
optionIf(!existIds.contains(commit.id)){ }
createIssueComment(commit) } catch {
Some(commit) case ex: Exception => {
} logger.error(ex.toString, ex)
} throw ex
} else { }
commits.flatMap { commit => }
optionIf(!existsCommitId(owner, repository, commit.id)){ }
createIssueComment(commit)
Some(commit) def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
} try {
} using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
} val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command =>
// batch insert all new commit id logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
insertAllCommitIds(owner, repository, newCommits.map(_.id)) val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
// record activity val commits = if (refName(1) == "tags") {
if(refName(1) == "heads"){ Nil
command.getType match { } else {
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, userName, branchName) command.getType match {
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits) case ReceiveCommand.Type.DELETE => Nil
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, userName, branchName) case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
case _ => }
} }
} else if(refName(1) == "tags"){
command.getType match { // Retrieve all issue count in the repository
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits) val issueCount =
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, userName, branchName, newCommits) countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
case _ => countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
}
} // Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if(refName(1) == "heads"){ val newCommits = commits.flatMap { commit =>
command.getType match { if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
case ReceiveCommand.Type.CREATE | if (issueCount > 0) {
ReceiveCommand.Type.UPDATE | pushedIds.add(commit.id)
ReceiveCommand.Type.UPDATE_NONFASTFORWARD => createIssueComment(commit)
updatePullRequests(branchName) // close issues
case _ => if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
} closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
} }
}
// call web hook Some(commit)
val webHookURLs = getWebHookURLs(owner, repository) } else None
if(webHookURLs.nonEmpty){ }
val payload = WebHookPayload(
git, // record activity
getAccountByUserName(userName).get, if(refName(1) == "heads"){
command.getRefName, command.getType match {
getRepository(owner, repository, baseURL).get, case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
newCommits, case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
getAccountByUserName(owner).get) case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
case _ =>
callWebHook(owner, repository, webHookURLs, payload) }
} } else if(refName(1) == "tags"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
case _ =>
}
}
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE |
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(branchName)
case _ =>
}
}
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseUrl)){
callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
}
case _ =>
}
}
}
// update repository last modified time.
updateLastActivityDate(owner, repository)
} catch {
case ex: Exception => {
logger.error(ex.toString, ex)
throw ex
} }
} }
// update repository last modified time.
updateLastActivityDate(owner, repository)
} }
private def createIssueComment(commit: CommitInfo) = { private def createIssueComment(commit: CommitInfo) = {
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData => StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
val issueId = matchData.group(2) if(getIssue(owner, repository, issueId).isDefined){
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){ getAccountByMailAddress(commit.mailAddress).foreach { account =>
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
} }
} }
} }
@@ -173,15 +217,19 @@ class CommitLogHook(owner: String, repository: String, userName: 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

@@ -0,0 +1,133 @@
package ssh
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
import org.slf4j.LoggerFactory
import java.io.{InputStream, OutputStream}
import util.ControlUtil._
import org.eclipse.jgit.api.Git
import util.Directory._
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.command.UnknownCommand
import servlet.{Database, CommitLogHook}
import service.{AccountService, RepositoryService, SystemSettingsService}
import org.eclipse.jgit.errors.RepositoryNotFoundException
import javax.servlet.ServletContext
import model.profile.simple.Session
object GitCommand {
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
}
abstract class GitCommand(val context: ServletContext, val owner: String, val repoName: String) extends Command {
self: RepositoryService with AccountService =>
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
protected var err: OutputStream = null
protected var in: InputStream = null
protected var out: OutputStream = null
protected var callback: ExitCallback = null
protected def runTask(user: String)(implicit session: Session): Unit
private def newTask(user: String): Runnable = new Runnable {
override def run(): Unit = {
Database(context) withTransaction { implicit session =>
try {
runTask(user)
callback.onExit(0)
} catch {
case e: RepositoryNotFoundException =>
logger.info(e.getMessage)
callback.onExit(1, "Repository Not Found")
case e: Throwable =>
logger.error(e.getMessage, e)
callback.onExit(1)
}
}
}
}
override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER")
val thread = new Thread(newTask(user))
thread.start()
}
override def destroy(): Unit = {}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
}
override def setErrorStream(err: OutputStream): Unit = {
this.err = err
}
override def setOutputStream(out: OutputStream): Unit = {
this.out = out
}
override def setInputStream(in: InputStream): Unit = {
this.in = in
}
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
(implicit session: Session): Boolean =
getAccountByUserName(username) match {
case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account))
case None => false
}
}
class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
val upload = new UploadPack(repository)
upload.upload(in, out, err)
}
}
}
}
}
class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with SystemSettingsService with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
val receive = new ReceivePack(repository)
if(!repoName.endsWith(".wiki")){
val hook = new CommitLogHook(owner, repoName, user, baseUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
receive.receive(in, out, err)
}
}
}
}
}
class GitCommandFactory(context: ServletContext, baseUrl: String) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
logger.debug(s"command: $command")
command match {
case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(context, owner, repoName, baseUrl)
case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(context, owner, repoName, baseUrl)
case _ => new UnknownCommand(command)
}
}
}

View File

@@ -0,0 +1,62 @@
package ssh
import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback, Command}
import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants
import service.SystemSettingsService
class NoShell extends Factory[Command] with SystemSettingsService {
override def create(): Command = new Command() {
private var in: InputStream = null
private var out: OutputStream = null
private var err: OutputStream = null
private var callback: ExitCallback = null
override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER")
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
val message =
"""
| Welcome to
| _____ _ _ ____ _ _
| / ____| (_) | | | _ \ | | | |
| | | __ _ | |_ | |_) | _ _ ___ | | __ ___ | |_
| | | |_ | | | | __| | _ < | | | | / __| | |/ / / _ \ | __|
| | |__| | | | | |_ | |_) | | |_| | | (__ | < | __/ | |_
| \_____| |_| \__| |____/ \__,_| \___| |_|\_\ \___| \__|
|
| Successfully SSH Access.
| But interactive shell is disabled.
|
| Please use:
|
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message))
err.flush()
in.close()
out.close()
err.close()
callback.onExit(127)
}
override def destroy(): Unit = {}
override def setInputStream(in: InputStream): Unit = {
this.in = in
}
override def setOutputStream(out: OutputStream): Unit = {
this.out = out
}
override def setErrorStream(err: OutputStream): Unit = {
this.err = err
}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
}
}
}

View File

@@ -0,0 +1,23 @@
package ssh
import org.apache.sshd.server.PublickeyAuthenticator
import org.apache.sshd.server.session.ServerSession
import java.security.PublicKey
import service.SshKeyService
import servlet.Database
import javax.servlet.ServletContext
class PublicKeyAuthenticator(context: ServletContext) extends PublickeyAuthenticator with SshKeyService {
override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = {
Database(context) withTransaction { implicit session =>
getPublicKeys(username).exists { sshKey =>
SshUtil.str2PublicKey(sshKey.publicKey) match {
case Some(publicKey) => key.equals(publicKey)
case _ => false
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
package ssh
import javax.servlet.{ServletContext, ServletContextEvent, ServletContextListener}
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
import util.Directory
import service.SystemSettingsService
import java.util.concurrent.atomic.AtomicBoolean
object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = org.apache.sshd.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private def configure(context: ServletContext, port: Int, baseUrl: String) = {
server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(context))
server.setCommandFactory(new GitCommandFactory(context, baseUrl))
server.setShellFactory(new NoShell)
}
def start(context: ServletContext, port: Int, baseUrl: String) = {
if(active.compareAndSet(false, true)){
configure(context, port, baseUrl)
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
}
def stop() = {
if(active.compareAndSet(true, false)){
server.stop(true)
logger.info("SSH Server is stopped.")
}
}
def isActive = active.get
}
/*
* Start a SSH Server Daemon
*
* How to use:
* git clone ssh://username@host_or_ip:29418/owner/repository_name.git
*/
class SshServerListener extends ServletContextListener with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[SshServerListener])
override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings()
if(settings.ssh){
settings.baseUrl match {
case None =>
logger.error("Could not start SshServer because the baseUrl is not configured.")
case Some(baseUrl) =>
SshServer.start(sce.getServletContext,
settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
}
}
}
override def contextDestroyed(sce: ServletContextEvent): Unit = {
if(loadSystemSettings().ssh){
SshServer.stop()
}
}
}

View File

@@ -0,0 +1,36 @@
package ssh
import java.security.PublicKey
import org.slf4j.LoggerFactory
import org.apache.commons.codec.binary.Base64
import org.eclipse.jgit.lib.Constants
import org.apache.sshd.common.util.{KeyUtils, Buffer}
object SshUtil {
private val logger = LoggerFactory.getLogger(SshUtil.getClass)
def str2PublicKey(key: String): Option[PublicKey] = {
// TODO RFC 4716 Public Key is not supported...
val parts = key.split(" ")
if (parts.size < 2) {
logger.debug(s"Invalid PublicKey Format: ${key}")
return None
}
try {
val encodedKey = parts(1)
val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey))
Some(new Buffer(decode).getRawPublicKey)
} catch {
case e: Throwable =>
logger.debug(e.getMessage, e)
None
}
}
def fingerPrint(key: String): Option[String] = str2PublicKey(key) match {
case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey))
case None => None
}
}

View File

@@ -29,7 +29,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
/** /**
* Allows only the repository owner and administrators. * Allows only the repository owner and administrators.
*/ */
trait OwnerAuthenticator { self: ControllerBase with RepositoryService => trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -40,6 +40,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member =>
member.userName == x.userName && member.isManager == true
}) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -106,7 +109,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
} }
/** /**
* Allows only the repository owner and administrators. * Allows only the repository owner (or manager for group repository) and administrators.
*/ */
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
@@ -155,3 +158,24 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
} }
} }
} }
/**
* Allows only the group managers.
*/
trait GroupManagerAuthenticator { self: ControllerBase with AccountService =>
protected def managersOnly(action: => Any) = { authenticate(action) }
protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) }
private def authenticate(action: => Any) = {
{
defining(request.paths){ paths =>
context.loginAccount match {
case Some(x) if(getGroupMembers(paths(0)).exists { member =>
member.userName == x.userName && member.isManager
}) => action
case _ => Unauthorized()
}
}
}
}
}

View File

@@ -3,7 +3,8 @@ package util
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
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 org.eclipse.jgit.transport.RefSpec import scala.util.control.Exception._
import scala.language.reflectiveCalls
/** /**
* Provides control facilities. * Provides control facilities.
@@ -15,21 +16,19 @@ object ControlUtil {
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B = def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally { try f(resource) finally {
if(resource != null){ if(resource != null){
try { ignoring(classOf[Throwable]) {
resource.close() resource.close()
} catch {
case e: Throwable => // ignore
} }
} }
} }
def using[T](git: Git)(f: Git => T): T = def using[T](git: Git)(f: Git => T): T =
try f(git) finally git.getRepository.close try f(git) finally git.getRepository.close()
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T = def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
try f(git1, git2) finally { try f(git1, git2) finally {
git1.getRepository.close git1.getRepository.close()
git2.getRepository.close git2.getRepository.close()
} }
def using[T](revWalk: RevWalk)(f: RevWalk => T): T = def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
@@ -38,23 +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()
}
}
def executeIf(condition: => Boolean)(action: => Unit): Boolean =
if(condition){
action
true
} else false
def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
if(condition) action else None
} }

View File

@@ -2,6 +2,7 @@ package util
import java.io.File import java.io.File
import util.ControlUtil._ import util.ControlUtil._
import org.apache.commons.io.FileUtils
/** /**
* Provides directories used by GitBucket. * Provides directories used by GitBucket.
@@ -14,30 +15,28 @@ object Directory {
case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match { case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
// environment variable GITBUCKET_HOME // environment variable GITBUCKET_HOME
case Some(env) => new File(env) case Some(env) => new File(env)
// default is HOME/gitbucket // default is HOME/.gitbucket
case None => new File(System.getProperty("user.home"), "gitbucket") case None => {
val oldHome = new File(System.getProperty("user.home"), "gitbucket")
if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){
//FileUtils.moveDirectory(oldHome, newHome)
oldHome
} else {
new File(System.getProperty("user.home"), ".gitbucket")
}
}
} }
}).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.
@@ -45,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 {
@@ -63,9 +44,9 @@ object FileUtil {
if(dir.exists()){ if(dir.exists()){
FileUtils.deleteDirectory(dir) FileUtils.deleteDirectory(dir)
} }
try{ try {
action(dir) action(dir)
}finally{ } finally {
FileUtils.deleteDirectory(dir) FileUtils.deleteDirectory(dir)
} }
} }

View File

@@ -1,6 +1,9 @@
package util package util
import scala.util.matching.Regex import scala.util.matching.Regex
import scala.util.control.Exception._
import slick.jdbc.JdbcBackend
import servlet.Database
import javax.servlet.http.{HttpSession, HttpServletRequest} import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
@@ -8,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)
@@ -42,10 +48,8 @@ object Implicits {
sb.toString sb.toString
} }
def toIntOpt: Option[Int] = try { def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
Option(Integer.parseInt(value)) Integer.parseInt(value)
} catch {
case e: NumberFormatException => None
} }
} }

View File

@@ -11,17 +11,20 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
*/ */
object JGitUtil { object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
/** /**
* The repository data. * The repository data.
* *
@@ -32,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.
@@ -45,9 +52,10 @@ object JGitUtil {
* @param commitId the last commit id * @param commitId the last commit id
* @param committer the last committer name * @param committer the last committer name
* @param mailAddress the committer's mail address * @param mailAddress the committer's mail address
* @param linkUrl the url of submodule
*/ */
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
committer: String, mailAddress: String) committer: String, mailAddress: String, linkUrl: Option[String])
/** /**
* The commit data. * The commit data.
@@ -72,16 +80,12 @@ object JGitUtil {
rev.getFullMessage, rev.getFullMessage,
rev.getParents().map(_.name).toList) rev.getParents().map(_.name).toList)
val summary = defining(fullMessage.trim.indexOf("\n")){ i => val summary = getSummaryMessage(fullMessage, shortMessage)
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
val description = defining(fullMessage.trim.indexOf("\n")){ i => val description = defining(fullMessage.trim.indexOf("\n")){ i =>
optionIf(i >= 0){ if(i >= 0){
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} } else None
} }
} }
@@ -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.
@@ -104,6 +109,15 @@ object JGitUtil {
*/ */
case class TagInfo(name: String, time: Date, id: String) case class TagInfo(name: String, time: Date, id: String)
/**
* The submodule data
*
* @param name the module name
* @param path the path in the repository
* @param url the repository url of this module
*/
case class SubmoduleInfo(name: String, path: String, url: String)
/** /**
* Returns RevCommit from the commit or tag id. * Returns RevCommit from the commit or tag id.
* *
@@ -128,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(1000).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",
@@ -136,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 {
@@ -152,7 +166,7 @@ object JGitUtil {
} }
} }
} }
/** /**
* Returns the file list of the specified path. * Returns the file list of the specified path.
* *
@@ -162,7 +176,7 @@ object JGitUtil {
* @return HTML of the file list * @return HTML of the file list
*/ */
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)] val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new RevWalk(git.getRepository)){ revWalk => using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision) val objectId = git.getRepository.resolve(revision)
@@ -180,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
@@ -195,22 +209,30 @@ object JGitUtil {
}) })
} }
while (treeWalk.next()) { while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) // submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
} else None
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
} }
} }
} }
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) => list.map { case (objectId, fileMode, path, name, linkUrl) =>
FileInfo( defining(commits(path)){ commit =>
objectId, FileInfo(
fileMode == FileMode.TREE, objectId,
name, fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
commits(path).getCommitterIdent.getWhen, name,
commits(path).getShortMessage, commit.getCommitterIdent.getWhen,
commits(path).getName, getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commits(path).getCommitterIdent.getName, commit.getName,
commits(path).getCommitterIdent.getEmailAddress) commit.getCommitterIdent.getName,
commit.getCommitterIdent.getEmailAddress,
linkUrl)
}
}.sortWith { (file1, file2) => }.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match { (file1.isDirectory, file2.isDirectory) match {
case (true , false) => true case (true , false) => true
@@ -219,7 +241,18 @@ object JGitUtil {
} }
}.toList }.toList
} }
/**
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
defining(fullMessage.trim.indexOf("\n")){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
}
/** /**
* Returns the commit list of the specified branch. * Returns the commit list of the specified branch.
* *
@@ -325,27 +358,6 @@ object JGitUtil {
}.toMap }.toMap
} }
/**
* Get object content of the given id as String from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param large if false then returns None for the large file
* @return the object or None if object does not exist
*/
def getContent(git: Git, id: ObjectId, large: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(large == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
case e: MissingObjectException => None
}
/** /**
* Returns the tuple of diff of the given commit and the previous commit id. * Returns the tuple of diff of the given commit and the previous commit id.
*/ */
@@ -364,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 {
@@ -377,7 +394,7 @@ object JGitUtil {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
} else { } else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
})) }))
} }
(buffer.toList, None) (buffer.toList, None)
@@ -400,8 +417,8 @@ object JGitUtil {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else { } else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
} }
}.toList }.toList
} }
@@ -473,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))
@@ -491,7 +508,134 @@ object JGitUtil {
refUpdate.setNewObjectId(newHeadId) refUpdate.setNewObjectId(newHeadId)
refUpdate.update() refUpdate.update()
newHeadId.getName newHeadId
} }
/**
* Read submodule information from .gitmodules
*/
def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
val repository = git.getRepository
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
(try {
val config = new BlobBasedConfig(repository.getConfig(), bytes)
config.getSubsections("submodule").asScala.map { module =>
val path = config.getString("submodule", module, "path")
val url = config.getString("submodule", module, "url")
SubmoduleInfo(module, path, url)
}
} catch {
case e: ConfigInvalidException => {
logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e)
Nil
}
}).toList
} getOrElse Nil
}
/**
* Get object content of the given path as byte array from the Git repository.
*
* @param git the Git object
* @param revTree the rev tree
* @param path the path
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
@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(revTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
} flatMap { objectId =>
getContentFromId(git, objectId, fetchLargeFile)
}
}
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.
*
* @param git the Git object
* @param id the object id
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
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

@@ -13,12 +13,7 @@ object Keys {
/** /**
* Session key for the logged in account information. * Session key for the logged in account information.
*/ */
val LoginAccount = "LOGIN_ACCOUNT" val LoginAccount = "loginAccount"
/**
* Session key for the redirect URL.
*/
val Redirect = "REDIRECT"
/** /**
* Session key for the issue search condition in dashboard. * Session key for the issue search condition in dashboard.
@@ -47,11 +42,30 @@ object Keys {
} }
object Flash {
/**
* Flash key for the redirect URL.
*/
val Redirect = "redirect"
/**
* Flash key for the information message.
*/
val Info = "info"
}
/** /**
* Define request keys. * Define request 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

@@ -18,51 +18,56 @@ object LDAPUtil {
/** /**
* Try authentication by LDAP using given configuration. * Try authentication by LDAP using given configuration.
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage). * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
*/ */
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = { def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind( bind(
ldapSettings.host, host = ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
ldapSettings.bindDN.getOrElse(""), dn = ldapSettings.bindDN.getOrElse(""),
ldapSettings.bindPassword.getOrElse(""), password = ldapSettings.bindPassword.getOrElse(""),
ldapSettings.tls.getOrElse(false), tls = ldapSettings.tls.getOrElse(false),
ldapSettings.keystore.getOrElse("") keystore = ldapSettings.keystore.getOrElse(""),
) match { error = "System LDAP authentication failed."
case Some(conn) => { ){ conn =>
withConnection(conn) { conn => findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match { case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
case Some(userDN) => userAuthentication(ldapSettings, userDN, password) case None => Left("User does not exist.")
case None => Left("User does not exist.")
}
}
} }
case None => Left("System LDAP authentication failed.")
} }
} }
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = { private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind( bind(
ldapSettings.host, host = ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
userDN, dn = userDN,
password, password = password,
ldapSettings.tls.getOrElse(false), tls = ldapSettings.tls.getOrElse(false),
ldapSettings.keystore.getOrElse("") keystore = ldapSettings.keystore.getOrElse(""),
) match { error = "User LDAP Authentication Failed."
case Some(conn) => { ){ conn =>
withConnection(conn) { conn => findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute) match {
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { case Some(mailAddress) => Right(LDAPUserInfo(
case Some(mailAddress) => Right(mailAddress) userName = getUserNameFromMailAddress(userName),
case None => Left("Can't find mail address.") fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
} findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
} }.getOrElse(userName),
mailAddress = mailAddress))
case None => Left("Can't find mail address.")
} }
case None => Left("User LDAP Authentication Failed.")
} }
} }
private def bind(host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String): Option[LDAPConnection] = { 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)
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) { if (tls) {
// Dynamically set Sun as the security provider // Dynamically set Sun as the security provider
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()) Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider())
@@ -87,7 +92,9 @@ object LDAPUtil {
// Bind to the server // Bind to the server
conn.bind(LDAP_VERSION, dn, password.getBytes) conn.bind(LDAP_VERSION, dn, password.getBytes)
Some(conn) // Execute a given function and returns a its result
f(conn)
} catch { } catch {
case e: Exception => { case e: Exception => {
// Provide more information if something goes wrong // Provide more information if something goes wrong
@@ -96,20 +103,15 @@ object LDAPUtil {
if (conn.isConnected) { if (conn.isConnected) {
conn.disconnect() conn.disconnect()
} }
// Returns an error message
None Left(error)
} }
} }
} }
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = { /**
try { * Search a specified user and returns userDN if exists.
f(conn) */
} finally {
conn.disconnect()
}
}
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = { private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
@tailrec @tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = { def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
@@ -128,10 +130,20 @@ 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 =>
optionIf (results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} } else None
} }
private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None
}
case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String)
} }

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

@@ -3,6 +3,8 @@ package util
import java.net.{URLDecoder, URLEncoder} import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._ import util.ControlUtil._
import org.apache.commons.io.input.BOMInputStream
import org.apache.commons.io.IOUtils
object StringUtil { object StringUtil {
@@ -27,7 +29,12 @@ object StringUtil {
def escapeHtml(value: String): String = def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content)) /**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
* And if given bytes contains UTF-8 BOM, it's removed from returned string.
*/
def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
def detectEncoding(content: Array[Byte]): String = def detectEncoding(content: Array[Byte]): String =
defining(new UniversalDetector(null)){ detector => defining(new UniversalDetector(null)){ detector =>
@@ -38,4 +45,23 @@ object StringUtil {
case e => e case e => e
} }
} }
/**
* Extract issue id like ```#issueId``` from the given message.
*
*@param message the message which may contains issue id
* @return the iterator of issue id
*/
def extractIssueId(message: String): Iterator[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2))
/**
* Extract close issue id like ```close #issueId ``` from the given message.
*
* @param message the message which may contains close command
* @return the iterator of issue id
*/
def extractCloseId(message: String): Iterator[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
} }

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

@@ -16,8 +16,8 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if(mailAddress.isEmpty){ val src = if(mailAddress.isEmpty){
// by user name // by user name
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && getSystemSettings().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"""
} }
@@ -27,14 +27,14 @@ trait AvatarImageProvider { self: RequestCache =>
} else { } else {
// by mail address // by mail address
getAccountByMailAddress(mailAddress).map { account => getAccountByMailAddress(mailAddress).map { account =>
if(account.image.isEmpty && getSystemSettings().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(getSystemSettings().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

@@ -7,6 +7,8 @@ import org.parboiled.common.StringUtils
import org.pegdown._ import org.pegdown._
import org.pegdown.ast._ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer
import java.util.Locale
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.{RequestCache, WikiService} import service.{RequestCache, WikiService}
@@ -43,7 +45,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
(text, text) (text, text)
} }
val url = repository.url.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)
@@ -87,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')
@@ -99,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.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
} }
} }
@@ -110,6 +113,21 @@ class GitBucketHtmlSerializer(
printer.print(' ').print(name).print('=').print('"').print(value).print('"') printer.print(' ').print(name).print('=').print('"').print(value).print('"')
} }
private def printHeaderTag(node: HeaderNode): Unit = {
val tag = s"h${node.getLevel}"
val headerTextString = printChildrenToString(node)
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
printer.print(s"""<$tag class="markdown-head">""")
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
visitChildren(node)
printer.print(s"</$tag>")
}
override def visit(node: HeaderNode): Unit = {
printHeaderTag(node)
}
override def visit(node: TextNode): Unit = { override def visit(node: TextNode): Unit = {
// convert commit id and username to link. // convert commit id and username to link.
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
@@ -120,5 +138,16 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text) printWithAbbreviations(text)
} }
} }
}
object GitBucketHtmlSerializer {
private val Whitespace = "[\\s]".r
def generateAnchorName(text: String): String = {
val noWhitespace = Whitespace.replaceAllIn(text, "-")
val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH)
}
} }

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

@@ -1,71 +1,64 @@
@(account: Option[model.Account], info: Option[Any])(implicit context: app.Context) @(account: model.Account, info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main((if(account.isDefined) "Edit your profile" else "Create your account")){ @html.main("Edit your profile"){
@if(account.isDefined){ <div class="container">
<h3>Edit your profile</h3> <div class="row-fluid">
} else { <div class="span3">
<h3>Create your account</h3> @menu("profile", settings.ssh)
}
@helper.html.information(info)
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true">
<div class="row-fluid">
<div class="span6">
@if(account.isEmpty){
<fieldset>
<label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span>
</fieldset>
}
@if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset>
<label for="password" class="strong">
Password
@if(account.nonEmpty){
(input to change password)
}
:
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
}
<fieldset>
<label for="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
<span id="error-fullName" class="error"></span>
</fieldset>
<fieldset>
<label for="mailAddress" class="strong">Mail Address:</label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
</div> </div>
<fieldset class="margin"> <div class="span9">
@if(account.isDefined){ @helper.html.information(info)
<div class="pull-right"> <form action="@url(account.userName)/_edit" method="POST" validate="true">
<a href="@path/@account.get.userName/_delete" class="btn btn-danger" id="delete">Delete account</a> <div class="box">
<div class="box-header">Profile</div>
<div class="box-content">
<div class="row-fluid">
<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>
<label for="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value="@account.fullName"/>
<span id="error-fullName" class="error"></span>
</fieldset>
<fieldset>
<label for="mailAddress" class="strong">Mail Address:</label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.mailAddress"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(Some(account))
</fieldset>
</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>
<input type="submit" class="btn btn-success" value="Save"/> </div>
<a href="@url(account.get.userName)" class="btn">Cancel</a> </form>
} else { </div>
<input type="submit" class="btn btn-success" value="Create account"/> </div>
}
</fieldset>
</form>
} }
<script> <script>
$(function(){ $(function(){

View File

@@ -0,0 +1,140 @@
@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div class="container">
<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="span5">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span7">
<fieldset>
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="margin">
@if(account.isDefined){
<div class="pull-right">
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
</div>
}
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
@if(account.isDefined){
<a href="@url(account.get.userName)" class="btn">Cancel</a>
}
</fieldset>
</form>
</div>
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
'userName': userName
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false);
} else {
$('#error-memberName').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
return !(e.keyCode == 13);
});
$('#delete').click(function(){
return confirm('Once you delete this group, there is no going back.\nAre you sure?');
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $('button.active').filter(function(i, e){
return $(e).data('name') == userName;
}).attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

View File

@@ -1,48 +1,58 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context) @(account: model.Account, groupNames: List[String], active: String,
isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context)
@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>
} }
</ul> @if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
@body <li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_editgroup" class="btn">Edit Group</a>
</div>
</li>
}
</ul>
@body
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
@(account: model.Account, members: List[String])(implicit context: app.Context) @(account: model.Account, members: List[String], isGroupManager: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main(account, Nil, "members"){ @main(account, Nil, "members", isGroupManager){
@if(members.isEmpty){ @if(members.isEmpty){
No members No members
} else { } else {

View File

@@ -0,0 +1,14 @@
@(active: String, ssh: Boolean)(implicit context: app.Context)
@import context._
<div class="box">
<ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="profile"){ class="active"}>
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
</li>
@if(ssh){
<li@if(active=="ssh"){ class="active"}>
<a href="@path/@loginAccount.get.userName/_ssh">SSH Keys</a>
</li>
}
</ul>
</div>

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