Compare commits

..

368 Commits
1.13 ... 2.5

Author SHA1 Message Date
Naoki Takezoe
2a60f607ff Update README.md for 2.5 2014-11-04 01:38:29 +09:00
Naoki Takezoe
78f4d26aa0 Add version 2.5 2014-11-03 05:46:32 +09:00
Naoki Takezoe
f59e86f5ca (refs #529)Add icons 2014-11-03 05:25:03 +09:00
Naoki Takezoe
1c2af36c92 (refs #529)Mentioned filter 2014-11-03 04:35:41 +09:00
takezoe
badbe73f4e Fix for Firefox 2014-11-03 02:30:19 +09:00
takezoe
a9d58698cd (refs #529)Adjust bottom line 2014-11-03 01:40:47 +09:00
Naoki Takezoe
bb3f086aa6 (refs #529)Enhance dashboard header 2014-11-03 00:31:34 +09:00
Naoki Takezoe
2db674bb03 (refs #529)Organization filter 2014-11-02 13:26:46 +09:00
Naoki Takezoe
4bc4a16a80 Merge branch 'master' into newui-for-dashboard
Conflicts:
	src/main/twirl/dashboard/issueslist.scala.html
	src/main/twirl/dashboard/pullslist.scala.html
2014-11-01 03:14:19 +09:00
Naoki Takezoe
d88a105628 Merge pull request #512 from mrkm4ntr/create-branch-ui
(refs #394) Create branch from Web UI
2014-11-01 03:10:38 +09:00
Naoki Takezoe
15d0c5b506 Merge pull request #526 from mrkm4ntr/datetime-format
(ref #519) Change datetime formats
2014-11-01 03:10:18 +09:00
Naoki Takezoe
dbde79d2f2 Merge pull request #342 from bati11/feature-tasklist
Implement "Task List" in markdown
2014-11-01 03:09:24 +09:00
Naoki Takezoe
e6e3786b47 (refs #529)Visibility filter 2014-11-01 03:05:52 +09:00
Tomofumi Tanaka
4c1b8004fc (refs #533)Admin user must not disable self account yourself 2014-10-29 09:15:20 +09:00
Naoki Takezoe
13c206d068 Applying new Issues UI to dashboard 2014-10-19 21:34:12 +09:00
Shintaro Murakami
5b875d7c73 Refactored, sorry. 2014-10-19 01:22:31 +09:00
Shintaro Murakami
e33dd9008b (ref #519) Change datetime formats 2014-10-18 23:21:47 +09:00
Naoki Takezoe
8764910553 (refs #504)Fix word-break of "Pages" table 2014-10-18 19:26:11 +09:00
Naoki Takezoe
4c89c40944 (refs #522)Recover user filter in dashboard 2014-10-18 19:11:30 +09:00
Shintaro Murakami
0f0986afcf (refs #394)Create branch from Web UI 2014-10-16 22:03:49 +09:00
Shintaro Murakami
5d5f1f8bdd (refs #514) Fix problems of renaming repository. 2014-10-16 22:03:49 +09:00
Naoki Takezoe
03e386b3ce Merge pull request #515 from mrkm4ntr/hotfix-514
(refs #514) Fix problems of renaming repository.
2014-10-12 19:05:54 +09:00
takezoe
435eac7ae6 (refs #511)Fix problem which is not possible to choose color at the colorpicker 2014-10-12 16:29:41 +09:00
takezoe
bd5df3977d (refs #518)Compile for Java7 2014-10-12 15:49:49 +09:00
bati11
ba218053f9 Modify to correspond to that "issuedetail.scala.html" has been deleted 2014-10-11 13:50:32 +09:00
bati11
1fe448a83b Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/issuedetail.scala.html
2014-10-11 12:07:14 +09:00
Shintaro Murakami
26a45d0117 (refs #514) Fix problems of renaming repository. 2014-10-09 22:05:42 +09:00
Naoki Takezoe
320585a530 Fix presentation problem on Firefox 2014-10-07 14:52:11 +09:00
Naoki Takezoe
ca0f888a99 Update for 2.4.1 2014-10-06 13:56:33 +09:00
Naoki Takezoe
3b08dc2e41 (refs #510)Fix dropdown presentation 2014-10-06 13:47:41 +09:00
Naoki Takezoe
cc128a49c1 (refs #510)Dirty fix for Firefox 2014-10-06 13:37:32 +09:00
Naoki Takezoe
e0148695f2 (refs #509)Fix link broken bug in Wiki 2014-10-06 13:22:42 +09:00
Naoki Takezoe
afe0b1dd71 Fix link bug in pull requests 2014-10-06 03:08:35 +09:00
Naoki Takezoe
353852d6da Update README.md 2014-10-06 02:14:10 +09:00
Naoki Takezoe
28585d1a3d Merge branch 'new-issue-ui' 2014-10-06 01:54:19 +09:00
shimamoto
9d69a48c65 (refs #488) Fix bug for refer comment. 2014-10-06 01:40:23 +09:00
shimamoto
2f95c76634 (refs #488) Fixed compile error. 2014-10-06 01:39:42 +09:00
shimamoto
eac9f0e6ff (refs #488) Fixed the action for assignee. 2014-10-06 01:30:30 +09:00
Naoki Takezoe
043fc21e05 Add Version 2.4 2014-10-06 01:05:40 +09:00
shimamoto
5854a75615 (refs #488) Fixed the action for label and milestone. 2014-10-06 00:52:14 +09:00
Naoki Takezoe
7b02946496 (refs #506)Fix generated URL for images 2014-10-05 23:07:15 +09:00
Naoki Takezoe
70f0ffd4f4 Fix label text color 2014-10-05 20:01:34 +09:00
Naoki Takezoe
91b82c2652 (refs #505)Disable the plugin system in default 2014-10-05 18:08:33 +09:00
shimamoto
b1017140aa (refs #488) Fixed the action for issue and comment content change. 2014-10-05 17:13:58 +09:00
takezoe
fc806b8813 Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-05 15:37:22 +09:00
takezoe
836913482b (refs #488)Issue label editing is completed 2014-10-05 15:37:09 +09:00
Naoki Takezoe
b3df3f44c6 Merge pull request #500 from mrkm4ntr/split-diff-style
Modify styles of split diff.
2014-10-05 11:56:49 +09:00
shimamoto
4ffbf89e74 (refs #488) Fixed the action for issue title change. 2014-10-05 04:43:11 +09:00
shimamoto
9851c7d93d (refs #488) Changing the issue edit. 2014-10-04 23:53:37 +09:00
Tomofumi Tanaka
2201f2b202 Simplify query 2014-10-04 00:31:23 +09:00
takezoe
c92e71bb7a Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-01 02:25:43 +09:00
takezoe
d271fac350 (refs #488)Add transparent label icons 2014-10-01 02:25:19 +09:00
Naoki Takezoe
ce4522fc30 (refs #488)Remove top margin of the clear condition link 2014-09-30 02:31:11 +09:00
Tomofumi Tanaka
a178c48de6 Merge branch 'fix-498-dash-pr' 2014-09-29 20:51:32 +09:00
Tomofumi Tanaka
9d1323a044 (refs #498)Reformat counting pull request query 2014-09-29 16:38:12 +09:00
Tomofumi Tanaka
43babfed94 (refs #498) Correct pull request counts 2014-09-29 14:00:29 +09:00
Tomofumi Tanaka
6fa7ea30fb (refs #498)Returns private own repository
RepositoryService.getAllRepositories should returns
private own repository too.
2014-09-29 11:53:19 +09:00
Tomofumi Tanaka
d78315695b (refs $#498)Don't show private repo user doesn't have permission
This fix in the dashboard pull request view
But this fix still has a problem show wrong count number pull request.
2014-09-29 11:27:48 +09:00
shimamoto
16021865cb (refs #488) Fixed the screen layout. 2014-09-28 21:15:30 +09:00
Naoki Takezoe
b516be242d (refs #488)Show number of issues for each labels 2014-09-28 11:43:58 +09:00
Naoki Takezoe
0124f7cc3c (refs #488)Change permission to access to labels 2014-09-28 11:35:19 +09:00
Naoki Takezoe
f3eec35287 (refs #488)Fix nav-pills style in the header of the issues system 2014-09-28 11:24:27 +09:00
Naoki Takezoe
fb396a33b0 (refs #488)Remove unnecessary div 2014-09-28 03:05:04 +09:00
Naoki Takezoe
3370499421 (refs #488)Apply new UI to labels 2014-09-28 03:04:36 +09:00
shimamoto
d847e27cf9 (refs #488) Move Milestone and Assignee. 2014-09-27 23:48:38 +09:00
Naoki Takezoe
9684b158ce (refs #488)Apply new UI to the milestone create / edit page 2014-09-26 02:14:30 +09:00
Naoki Takezoe
8456808a8e (refs #488)Add milestone icons 2014-09-25 09:37:13 +09:00
Naoki Takezoe
9747899a19 (refs #488)Update icons 2014-09-25 09:36:48 +09:00
shimamoto
099304605e Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-09-25 04:49:07 +09:00
shimamoto
30994d0465 (refs #488) WIP: Fixing the screen layout for the issue detail. 2014-09-25 04:48:39 +09:00
Naoki Takezoe
71fdbe7b71 (refs #488)Apply new UI to Milestones tab 2014-09-25 02:32:55 +09:00
Naoki Takezoe
86432c5ffe (refs #488)Small fix 2014-09-25 01:52:32 +09:00
takezoe
4dfa1fb0f8 Merge remote-tracking branch 'origin/new-issue-ui' into new-issue-ui 2014-09-23 23:26:54 +09:00
takezoe
db59a7652f (refs #488)Add icons for new Issues UI 2014-09-23 23:26:46 +09:00
shimamoto
417470a81c (refs #488) Fixed the screen layout for the issue creation. 2014-09-23 21:42:56 +09:00
Naoki Takezoe
cc639da17e (refs #488)Unify issues and pull requests template 2014-09-23 18:11:46 +09:00
Naoki Takezoe
f619f4a9bc (refs #488)Remove unnecessary template arguments 2014-09-23 17:50:47 +09:00
Naoki Takezoe
5dffc2a64e (refs #488)Batch edit for pull requests 2014-09-23 17:41:53 +09:00
Naoki Takezoe
bb63a8d14c (refs #488)Remove unnecessary template arguments 2014-09-23 17:23:52 +09:00
Naoki Takezoe
c1263cc16d (refs #488)Batch edit for issues 2014-09-23 14:57:29 +09:00
Shintaro Murakami
49f2e7d70f Add empty style. 2014-09-23 02:10:46 +09:00
Shintaro Murakami
f93b535f70 Modify styles of split diff. 2014-09-23 00:07:00 +09:00
shimamoto
e16d3c823b Update slick version. 2014-09-22 21:59:17 +09:00
Naoki Takezoe
7a6fdbcf50 (refs #488)Fix the "New pull request" button 2014-09-22 19:26:36 +09:00
Naoki Takezoe
46041a3762 (refs #488)Apply new UI to the pull request list 2014-09-22 19:10:39 +09:00
Naoki Takezoe
20b0553f7f (refs #488)Exclude pull requests from the issue list 2014-09-22 18:38:28 +09:00
Naoki Takezoe
5870cacf44 (refs #488)Fix presentation of issue list header 2014-09-22 18:29:30 +09:00
Naoki Takezoe
cb512cd98d (refs #488)Add link to clear search condition 2014-09-22 10:18:26 +09:00
Naoki Takezoe
90487eb7b7 (refs #488)Merge user filter into IssueSearchCondition 2014-09-22 10:08:22 +09:00
Naoki Takezoe
706fa77de3 (refs #488)Add service.IssuesService.IssueInfo 2014-09-21 02:06:38 +09:00
bati11
26b14ded58 Add nested task list support 2014-09-20 10:57:33 +09:00
Naoki Takezoe
3b1367dd8e (refs #488)Displays the milestone on the issue list. 2014-09-20 03:02:52 +09:00
bati11
e1f310317d Modify GitBucketHtmlSerializer constructor parameters
- Add to the GitBucketHtmlSerializer constructor parameter "hasWritePermission"
- Remove the call to the RepositoryService.hasWritePermission in GitBucketHtmlSerializer
2014-09-19 14:13:53 +09:00
bati11
937814ec5d Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/scala/app/IssuesController.scala
	src/main/twirl/issues/create.scala.html
2014-09-19 12:45:09 +09:00
bati11
b55fc649a6 Change crlf to lf 2014-09-19 12:43:04 +09:00
Naoki Takezoe
f4e4506517 (refs #488)Start to apply new issue UI to the issue list. 2014-09-16 00:46:38 +09:00
Naoki Takezoe
287a0b6669 (refs #488)Copy listparts.scala.html from issues/pulls to dashboard. 2014-09-15 23:47:06 +09:00
Naoki Takezoe
5bddd352af (refs #453)Fix "Test Hook" behavior 2014-09-15 13:04:39 +09:00
Naoki Takezoe
9c6ea8fb9d Fix client side validation error 2014-09-15 12:39:30 +09:00
Naoki Takezoe
32e8bf46a7 Merge pull request #491 from mrkm4ntr/myBranch
Change to show a shortcut path of the directory whose ancestors have only one child directory.
2014-09-15 02:12:52 +09:00
Naoki Takezoe
d61fe1bf84 (refs #483)Fix link in markdown 2014-09-14 20:31:02 +09:00
Naoki Takezoe
47dbea947d (refs #487) split diff is available 2014-09-14 10:53:17 +09:00
Naoki Takezoe
97c6b0495e Fix Warnings 2014-09-13 19:01:49 +09:00
Tomofumi Tanaka
a602ece8e9 (refs #490)Set HEAD ref when saved default branch
GitBucket allows user to configure default branch in the repository.
But it's only affect repository viewer in the GitBucekt world.
This change set default branch in the Git world.
2014-09-12 22:01:21 +09:00
Shintaro Murakami
cf6dca84d8 Release TreeWalk in recuresive function. 2014-09-11 00:06:28 +09:00
Shintaro Murakami
79432ff8ad Like GitHub, show a shortcut path of the directory whoes ancestors have only one child directory. 2014-09-10 07:07:04 +09:00
tanacasino
b8613431de Merge pull request #484 from douglarek/patch-1
Use a shebang (#!/bin/sh) to run sbt.sh
2014-09-04 11:42:17 +09:00
Lingchao Xin
698eafa562 Use a shebang (#!/bin/sh) to run sbt.sh
If no shebang provided in a shell file, it will be executed by default shell.

However this script is not compatible with Fish Shell, so use sh to execute it.
2014-09-04 10:25:24 +08:00
Naoki Takezoe
d33886db89 Add RawData and 404 error response for plugin action 2014-09-03 02:26:06 +09:00
Naoki Takezoe
cde09d3a59 Fix plugin path problem 2014-09-03 01:46:37 +09:00
Naoki Takezoe
5674f0e980 Update README.md 2014-09-01 14:34:58 +09:00
Naoki Takezoe
b9ade60eb2 (refs #464)Improve plugin installing/updating behavior 2014-09-01 01:11:19 +09:00
Naoki Takezoe
96303723fa (refs #464)Clear plugins before upgrading to 2.3. 2014-09-01 00:29:13 +09:00
Naoki Takezoe
0f5dbc5788 Merge branch 'master' into scala-plugin
Conflicts:
	project/build.scala
2014-09-01 00:02:31 +09:00
Naoki Takezoe
8df0c3a439 (refs #476)Change Jetty temp directory to GITBUCKET_HOME/tmp 2014-08-31 23:31:59 +09:00
Tomofumi Tanaka
ca6a86816a (refs #461)Correct atom feed datetime format 2014-08-25 22:32:07 +09:00
Tomofumi Tanaka
3ea939798f Remove unused import 2014-08-24 22:18:21 +09:00
Tomofumi Tanaka
d947410e3c (refs #434)Refactor to get last modified commit 2014-08-24 22:18:21 +09:00
Tomofumi Tanaka
db59bc08ac (refs #434)Show only last modified commit 2014-08-24 22:18:14 +09:00
Naoki Takezoe
95a8649f79 (refs #464)Add Fragment rendering support for Ajax 2014-08-24 13:56:49 +09:00
Naoki Takezoe
ffd10122ed (refs #464)Some improve for plugin API
- Place holder support for db API
- Redirect support for plugin action
2014-08-23 17:57:06 +09:00
Naoki Takezoe
c4c39f36e9 (refs #464)Add db.update() to update DB from plugin 2014-08-23 03:28:13 +09:00
Naoki Takezoe
96900c3cbf (refs #464)Remove unnecessary App mix-in 2014-08-23 03:26:19 +09:00
Tomofumi Tanaka
69fa370d12 Tweak font size and family in blog/diff view 2014-08-22 00:02:58 +09:00
Tomofumi Tanaka
7496437d11 Remove unused h7 2014-08-20 22:34:48 +09:00
shimamoto
33b7d09af7 Update slick version to 2.1.0. 2014-08-17 18:25:06 +09:00
Naoki Takezoe
53d0974760 (refs #457)Fix the target of updateRef 2014-08-17 13:01:05 +09:00
Naoki Takezoe
a87399f223 (refs #464)Add a new parameter to specify request method for plugin actions 2014-08-16 16:02:02 +09:00
Naoki Takezoe
975dfb17e1 (refs #464)Twirl support for plugin 2014-08-16 02:53:43 +09:00
Naoki Takezoe
8b8bd0289b (refs #464)Fix test case 2014-08-14 22:44:04 +09:00
Naoki Takezoe
3bb69c623b (refs #464)Switch to play-twirl 2014-08-14 18:37:37 +09:00
Naoki Takezoe
dd427bdbef Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-08-14 14:01:33 +09:00
Naoki Takezoe
b40657a14a (refs #467)Reverse tag table ordering 2014-08-14 14:01:11 +09:00
Tomofumi Tanaka
21ca5b2eec (refs #471)Show the copy button only when flash is available
Check flash availability before Showing the copy button.
2014-08-14 12:43:55 +09:00
Naoki Takezoe
b78d584d8a (refs #464)Add drop tables capability when plugin is uninstalled 2014-08-14 01:56:47 +09:00
Naoki Takezoe
e6b666a66a (refs #464)Implementing database migration system for plugin 2014-08-13 23:16:13 +09:00
Naoki Takezoe
bab93ea4f5 (refs #464)Fix compilation error 2014-08-13 22:06:08 +09:00
Naoki Takezoe
7fe98253ae Merge pull request #452 from mslinn/master
Enhanced install script so it works under Ubuntu and Mac OS/X
2014-08-13 15:38:44 +09:00
Naoki Takezoe
13385cbced (refs #464)Add PLUGIN table for plugin management 2014-08-13 02:23:29 +09:00
Naoki Takezoe
3f20cec7b2 (refs #464)Add Scaladoc 2014-08-12 00:37:36 +09:00
Naoki Takezoe
a0e4b020ca (refs #464)Authentication for actions which are defined by plugin is completed 2014-08-12 00:33:13 +09:00
Naoki Takezoe
ea5d898b27 (refs #464)Add Security sealed trait which is used by plugin 2014-08-12 00:02:48 +09:00
Naoki Takezoe
4e652b5ccd (refs #464)Add authentication for plugin action 2014-08-11 19:27:24 +09:00
Naoki Takezoe
dd809896c8 (refs #464)Add extension point to inject JavaScript instead of adding button 2014-08-11 00:45:58 +09:00
Naoki Takezoe
93536d3365 (refs #464)Add new extension point to add buttons 2014-08-10 05:42:06 +09:00
Naoki Takezoe
098b18fe6d (refs #464)Experimental implementation of Scala based plugin 2014-08-10 04:33:57 +09:00
tanacasino
66efdac757 Merge pull request #449 from jparound30/fix_423
Change blob view's table-layout property.
2014-08-07 23:08:54 +09:00
Tomofumi Tanaka
45545d3815 Revert "(refs #458)Skip unexpected commit message"
This reverts commit be79ac2eb2.
2014-08-05 23:02:31 +09:00
Tomofumi Tanaka
b65d41731b (refs #458)Correct commit message in activity info 2014-08-05 23:00:56 +09:00
Tomofumi Tanaka
be19e97518 Merge branch '2.2-update' 2014-08-05 08:53:51 +09:00
Naoki Takezoe
2ebf2b99bd Update README.md 2014-08-05 02:15:36 +09:00
Tomofumi Tanaka
be79ac2eb2 (refs #458)Skip unexpected commit message
This patch is temporary measures.
MUST create AutoUpdate before release 2.3.
2014-08-05 00:43:20 +09:00
Tomofumi Tanaka
05afec3236 (refs#458)Correct short and full message
Swaped short and full message in commit info by accident.
2014-08-05 00:07:21 +09:00
Naoki Takezoe
57879eb72e Update README.md 2014-08-04 00:08:05 +09:00
Naoki Takezoe
2bc915f51b Disable JavaScript console 2014-08-04 00:00:41 +09:00
Naoki Takezoe
1ca55805b5 JSON response support for plug-ins 2014-08-03 23:59:56 +09:00
Naoki Takezoe
93cc1be166 (refs #374)Fix build.xml 2014-08-03 19:26:01 +09:00
Naoki Takezoe
f88ce3f671 Update version number to 2.2 2014-08-03 19:19:15 +09:00
Naoki Takezoe
20aabfc273 Merge branch 'scala-2.11' 2014-08-03 19:13:34 +09:00
shimamoto
601f8c4249 (refs #374) Fix compile error. 2014-08-03 18:41:03 +09:00
Tomofumi Tanaka
d0ccfc52b8 (refs #421)Add tar.gz archive download link 2014-08-02 21:22:49 +09:00
Tomofumi Tanaka
c22aef8ee2 (refs #421)Add tar.gz archive download
* Update jgit version
* Add new lib org.eclipse.jgit.archive
* TODO: Add link in views
2014-08-01 23:56:02 +09:00
Tomofumi Tanaka
3807e61a48 Merge branch 'show-author' 2014-07-31 22:48:15 +09:00
Naoki Takezoe
55722f87af Fix TestCase 2014-07-31 22:04:52 +09:00
Tomofumi Tanaka
212f3725ed (refs #437)Show author at blob, commits and wiki history view 2014-07-30 22:41:04 +09:00
Mike Slinn
193a312b22 Made gitbucket run on system startup and stop on shutdown 2014-07-29 15:49:48 -07:00
Mike Slinn
6a2d2ebfd1 Added help info for user about making iptables changes persistent 2014-07-29 15:44:43 -07:00
Naoki Takezoe
82beed1f44 (refs #374)Upgrading to Scala 2.11.2 2014-07-30 07:43:32 +09:00
Naoki Takezoe
0ede7e9921 (refs #374)Upgrading to Scalatra 2.3 and Slick 2.1.
Some compilation errors in Slick code are remaining.
2014-07-30 07:36:35 +09:00
Mike Slinn
6d200aa340 Works under Ubuntu and Mac OS/X 2014-07-29 07:28:44 -07:00
Mike Slinn
a0fbb90048 Works on Mac, need to retest on Ubuntu 2014-07-29 00:18:09 -07:00
Mike Slinn
08e29e7077 Added install script, made existing RedHat init script also work with Ubuntu 2014-07-28 10:10:27 -07:00
Tomofumi Tanaka
d2317d0a97 (refs #437)Fix typo
Threre is more good function name, but i have no idea 😵
2014-07-29 01:24:09 +09:00
Tomofumi Tanaka
972628eb65 (refs #437)Show author and committer at files view 2014-07-29 01:11:27 +09:00
Tomofumi Tanaka
51a56356cb (refs #437)Show author at repo file list view 2014-07-29 00:56:34 +09:00
jparound30
3bef71f5f2 (refs #423)Change blob view's table-layout property. 2014-07-29 00:22:22 +09:00
Tomofumi Tanaka
2bb1f6168a (refs #437)Show author instead of committer
* Explicit classify committer and author
* Use author to render avatar image html
* Support commit view
2014-07-29 00:06:45 +09:00
shimamoto
b13820fc0e Improved model package. The details are as follows:
* Fix the Profiles class from package object to simple object
* Fix the row case class to model package
* Define the alias of JdbcBackend#Session
2014-07-28 04:52:56 +09:00
takezoe
723de9e81e Fix TestCase 2014-07-27 22:56:02 +09:00
takezoe
3e161353ed Merge branch 'slick2-compilation-problem' 2014-07-27 21:24:40 +09:00
takezoe
2a8706630a (refs #445)Fix yen to backslash 2014-07-27 17:11:27 +09:00
Naoki Takezoe
121b6ee641 Fix incremental compilation problem caused by Slick.
This is temporary fix to decrease compilation time in development. Therefore this fix will be reverted in the future to add multiple database support capability.
2014-07-27 03:31:45 +09:00
Naoki Takezoe
34e299bf52 (refs #445)Keep line separator in online file editing 2014-07-26 23:15:47 +09:00
Naoki Takezoe
0822b7b5f3 (refs #444)Fix pull request count in dashboard 2014-07-26 19:12:09 +09:00
Naoki Takezoe
618110327a (refs #443)Fix merge guidance 2014-07-26 04:00:15 +09:00
Naoki Takezoe
f58f476060 (refs #441)Upgrade h2 to the latest version. 2014-07-25 15:46:29 +09:00
Naoki Takezoe
f5a544603a Experimental JDBC API for JavaScript plug-ins 2014-07-24 01:39:26 +09:00
Naoki Takezoe
89515cd087 Add an argument RepositoryInfo to RepositoryAction 2014-07-24 00:57:21 +09:00
Naoki Takezoe
37731c4163 Enable plugin system 2014-07-23 19:50:38 +09:00
Naoki Takezoe
1d4720d784 Merge pull request #439 from jmu/master
Fix IE copy not working
2014-07-23 01:30:21 +09:00
jmu
a10b053489 fix ie copy not working 2014-07-22 19:50:02 +08:00
takezoe
6122c8a1e1 Fix #438 2014-07-21 03:31:52 +09:00
Tomofumi Tanaka
fa9254c240 (refs #435)Correct merge commit message
Use ${user}/${branch} instead of ${user}/{repoName}
2014-07-16 23:33:14 +09:00
Tomofumi Tanaka
10616bca7d (refs #436)Encode branch name to delete at pullreq view 2014-07-16 19:24:25 +09:00
Naoki Takezoe
307f7e15e9 (refs #431)Fix CSS layout in Wiki page 2014-07-15 07:07:52 +09:00
Naoki Takezoe
86cf97d76b Fix TODO: Close issue and send webhook from online editing 2014-07-14 01:10:33 +09:00
shimamoto
01f6590c04 Fix TODO. 2014-07-14 00:08:34 +09:00
shimamoto
8f0c22bae9 Improve slick session(because transaction for unnecessary). 2014-07-13 23:54:53 +09:00
Naoki Takezoe
652a68c5b1 Fix TODO 2014-07-13 23:20:16 +09:00
Naoki Takezoe
1f56e1360d Fix font size 2014-07-13 16:31:24 +09:00
Naoki Takezoe
38475ffefe Fix avatar image size 2014-07-13 16:26:57 +09:00
Naoki Takezoe
7a44a4d726 Merge branch '401-news-feed-of-private-repo' of https://github.com/utensil/gitbucket into utensil-401-news-feed-of-private-repo
Conflicts:
	src/main/scala/app/IndexController.scala
	src/main/scala/service/ActivityService.scala
2014-07-13 16:07:42 +09:00
Naoki Takezoe
9dbc0c3fd6 Change LDAPUtil method name 2014-07-13 15:53:12 +09:00
Naoki Takezoe
56bb43ea6b AccountUtil is merged to LDAPUtil 2014-07-13 15:13:59 +09:00
Naoki Takezoe
b287c1f60d Merge branch 'add-features-to-ldapauth' of https://github.com/yjkony/gitbucket into yjkony-add-features-to-ldapauth
Conflicts:
	src/main/scala/app/IndexController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/main/scala/util/LDAPUtil.scala
	src/main/scala/util/Notifier.scala
2014-07-13 13:49:04 +09:00
Naoki Takezoe
258d53b7a6 Disable submit buttons while performing validation 2014-07-13 03:48:44 +09:00
Naoki Takezoe
2e11d6dd78 Disable submit buttons while performing validation 2014-07-13 03:47:40 +09:00
Tomofumi Tanaka
a2a2e22485 Fix compilation error service test case 2014-07-09 00:25:40 +09:00
Tomofumi Tanaka
c182cde14b Revert "Disable TestCase for Services"
This reverts commit 104c3bc89d.
2014-07-08 23:55:15 +09:00
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
utensil
3683a5fb7d Show newsfeed of private repo to members of owner group 2014-06-22 09:59:59 +00:00
utensil
1223bf2fd8 Show newsfeed of private repo to its owner 2014-06-22 08:10:37 +00: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
yjkony
e2c99a46be Merge commit Tag 2.0 'db5395ddbc4aef485415408720dd09cfc215b527' into add-features-to-ldapauth
Conflicts:
	src/main/twirl/account/edit.scala.html
2014-06-02 17:01:22 +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
bati11
6175eb7c08 Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/commentform.scala.html
	src/main/twirl/issues/create.scala.html
	src/main/twirl/pulls/compare.scala.html
	src/main/twirl/wiki/edit.scala.html
2014-05-31 12:17:30 +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
bati11
ebb9d9329a Merge branch 'master' into feature-tasklist 2014-05-03 10:51:18 +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
yjkony
8c35310cd6 Merge commit Tag 1.13 ('3e82534c78a72e17dd3b79e091521d75cb4d3855') into add-features-to-ldapauth
Conflicts:
	src/main/scala/service/AccountService.scala
	src/main/scala/util/LDAPUtil.scala
2014-05-01 11:56:56 +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
bati11
843722f82e Implement the feature "Task List" 2014-04-10 02:08:45 +09:00
bati11
ce79eaada8 Add escapeTaskList method, it escapse '- [] ' characters 2014-04-10 01:21:55 +09:00
yjkony
00af52815d Merge commit '5317ac5e031a29438657952fb882532af296135b' (tag 1.12) into add-features-to-ldapauth 2014-03-31 12:37:23 +09:00
yjkony
9175cf5c71 Merge branch 'master' into add-features-to-ldapauth
Conflicts:
	src/main/twirl/account/edit.scala.html
2014-03-14 15:56:08 +09:00
yjkony
a74bbd3eeb Merge branch 'master' into add-features-to-ldapauth 2014-03-13 18:10:41 +09:00
yjkony
4e2a3fdbd0 Change trigger of "Disalbe mail resolve is enalbed" from "When system settings check-box is ON" to "When mail attribute is empty". 2014-03-11 12:56:00 +09:00
yjkony
8d200c72d3 Merge branch 'master' into add-features-to-ldapauth 2014-03-10 11:05:02 +09:00
yjkony
18cd967a9c Modify wrong label of "Additional filter condition" label in system settings page 2014-03-06 11:03:04 +09:00
yjkony
328d6c1d17 Merge branch 'master' into add-features-to-ldapauth 2014-03-06 10:52:16 +09:00
yjkony
a335c31385 Revert unnecessary format changes. 2014-03-04 10:54:54 +09:00
yjkony
97349a9bb2 Merge branch 'master' into add-features-to-ldapauth 2014-03-04 10:16:24 +09:00
yjkony
ce3b6ed7c2 Revert line separator from LF to CRLF 2014-03-04 10:16:12 +09:00
yjkony
5e0619b500 Sync upstream/maste to master and Merge branch 'master' into add-features-to-ldapauth
Conflicts:
	src/main/scala/app/IndexController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/main/twirl/admin/system.scala.html
2014-03-03 15:46:38 +09:00
yjkony
639e7e0b3f Add features (additional filter condition / disable mail resolve) to LDAP authentication. 2014-02-28 21:32:42 +09:00
449 changed files with 105454 additions and 66768 deletions

View File

@@ -1,16 +1,14 @@
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.
[![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket)
Features 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 and online file editing - Repository viewer and online file editing
- Repository search (Code and Issues) - Repository search (Code and Issues)
- Wiki - Wiki
@@ -82,10 +80,53 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.5 - 4 Nov 2014
- New Dashboard
- Change datetime format
- Create branch from Web UI
- Task list in Markdown
- Some bug fix and improvements
### 2.4.1 - 6 Oct 2014
- Bug fix
### 2.4 - 6 Oct 2014
- New UI is applied to Issues and Pull requests
- Side-by-side diff is available
- Fix relative path problem in Markdown links and images
- Plugin System is disabled in default
- Some bug fix and improvements
### 2.3 - 1 Sep 2014
- Scala based plugin system
- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
- Some bug fix and improvements
### 2.2.1 - 5 Aug 2014
- Bug fix
### 2.2 - 4 Aug 2014
- Plug-in system is available
- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
- tar.gz export for repository contents
- LDAP authentication improvement (mail address became optional)
- Show news feed of a private repository to members
- Some bug fix and improvements
### 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 ### 1.13 - 29 Apr 2014
- Direct file editing in the repository viewer using AceEditor - Direct file editing in the repository viewer using AceEditor
- File attachment for issues - File attachment for issues
- Atom feed for user activities - Atom feed of user activity
- Fix some bugs - Fix some bugs
### 1.12 - 29 Mar 2014 ### 1.12 - 29 Mar 2014

View File

@@ -4,7 +4,7 @@
<property name="target.dir" value="target"/> <property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/> <property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/> <property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.10"/> <property name="scala.version" value="2.11"/>
<property name="gitbucket.version" value="0.0.1"/> <property name="gitbucket.version" value="0.0.1"/>
<property name="jetty.version" value="8.1.8.v20121106"/> <property name="jetty.version" value="8.1.8.v20121106"/>
<property name="servlet.version" value="3.0.0.v201112011016"/> <property name="servlet.version" value="3.0.0.v201112011016"/>
@@ -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">

13
contrib/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Contrib Notes #
The configuration script adapts according to the OS.
The `linux` directory contains scripts for Ubuntu and RedHat.
The Mac scripts have been folded in as well.
Common scripts are in this directory.
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
To run:
1. Edit `gitbucket.conf` to suit.
2. Type: `install`

62
contrib/gitbucket.conf Normal file
View File

@@ -0,0 +1,62 @@
# Configuration section is below. Ignore this part
function isUbuntu {
if [ -f /etc/lsb-release ]; then
grep -i ubuntu /etc/lsb-release | head -n 1 | cut -d \ -f 1 | cut -d = -f 2
fi
}
function isRedHat {
if [ -d "/etc/rc.d/init.d" ]; then echo yes; fi
}
function isMac {
if [[ "$(uname -a | cut -d \ -f 1 )" == "Darwin" ]]; then echo yes; fi
}
#
# Configuration section start
#
# Bind host
GITBUCKET_HOST=0.0.0.0
# Other Java option
GITBUCKET_JVM_OPTS=-Dmail.smtp.starttls.enable=true
# Data directory, holds repositories
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_LOG_DIR=/var/log/gitbucket
# Server port
GITBUCKET_PORT=8080
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
GITBUCKET_PREFIX=
# Directory where GitBucket is installed
# Configuration is stored here:
GITBUCKET_DIR=/usr/share/gitbucket
GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
# Path to the WAR file
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
# GitBucket version to fetch when installing
GITBUCKET_VERSION=2.1
#
# End of configuration section. Ignore this part
#
if [ `isUbuntu` ]; then
GITBUCKET_SERVICE=/etc/init.d/gitbucket
elif [ `isRedHat` ]; then
GITBUCKET_SERVICE=/etc/rc.d/init.d
elif [ `isMac` ]; then
GITBUCKET_SERVICE=/Library/StartupItems/GitBucket/GitBucket
else
echo "Don't know how to install onto this OS"
exit -2
fi

View File

@@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash
# #
# /etc/rc.d/init.d/gitbucket # RedHat: /etc/rc.d/init.d/gitbucket
# Ubuntu: /etc/init.d/gitbucket
# Mac OS/X: /Library/StartupItems/GitBucket
# #
# Starts the GitBucket server # Starts the GitBucket server
# #
@@ -8,28 +10,44 @@
# description: Run GitBucket server # description: Run GitBucket server
# processname: java # processname: java
# Source function library set -e
. /etc/rc.d/init.d/functions
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
[ -f /etc/rc.common ] && source /etc/rc.common # Mac OS/X
# Default values # Default values
GITBUCKET_HOME=/var/lib/gitbucket GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# Pull in cq settings # Pull in cq settings
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket [ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
[ -f gitbucket.conf ] && source gitbucket.conf # For all systems
# Location of the log and PID file # Location of the log and PID file
LOG_FILE=/var/log/gitbucket/run.log LOG_FILE=$GITBUCKET_LOG_DIR/run.log
PID_FILE=/var/run/gitbucket.pid PID_FILE=/var/run/gitbucket.pid
# Default return value RED='\033[1m\E[37;41m'
RETVAL=0 GREEN='\033[1m\E[37;42m'
OFF='\E[0m'
if [ -z "$(which success)" ]; then
function success {
printf "%b\n" "$GREEN $* $OFF"
}
fi
if [ -z "$(which failure)" ]; then
function failure {
printf "%b\n" "$RED $* $OFF"
}
fi
RETVAL=0
start() { start() {
echo -n $"Starting GitBucket server: " echo -n $"Starting GitBucket server: "
# Compile statup parameters START_OPTS=
if [ $GITBUCKET_PORT ]; then if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}" START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi fi
@@ -40,17 +58,15 @@ start() {
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}" START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi fi
# 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 &
RETVAL=$? RETVAL=$?
# Store PID of the Java process into a file
echo $! > $PID_FILE echo $! > $PID_FILE
if [ $RETVAL -eq 0 ] ; then if [ $RETVAL -eq 0 ] ; then
success "GitBucket startup" success "Success"
else else
failure "GitBucket startup" failure "Exit code $RETVAL"
fi fi
echo echo
@@ -82,25 +98,41 @@ restart() {
start start
} }
## MacOS proxies for System V service hooks:
case "$1" in StartService() {
start)
start start
;; }
stop)
StopService() {
stop stop
;; }
restart)
RestartService() {
restart restart
;; }
status)
status -p $PID_FILE java
RETVAL=$?
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
exit $RETVAL if [ `isMac` ]; then
RunService "$1"
else
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status -p $PID_FILE java
RETVAL=$?
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
exit $RETVAL
fi

69
contrib/install Executable file
View File

@@ -0,0 +1,69 @@
#!/bin/bash
# Only tested on Ubuntu 14.04
# Uses information stored in GitBucket git repo on GitHub as defaults.
# Edit gitbucket.conf before running this
set -e
GITBUCKET_VERSION=2.1
if [ ! -f gitbucket.conf ]; then
echo "gitbucket.conf not found, aborting"
exit -3
fi
source gitbucket.conf
function createDir {
if [ ! -d "$1" ]; then
echo "Making $1 directory."
sudo mkdir -p "$1"
fi
}
if [ "$(which iptables)" ]; then
echo "Opening port $GITBUCKET_PORT in firewall."
sudo iptables -A INPUT -p tcp --dport $GITBUCKET_PORT -j ACCEPT
echo "Please use iptables-persistent:"
echo " sudo apt-get install iptables-persistent"
echo "After installed, you can save/reload iptables rules anytime:"
echo " sudo /etc/init.d/iptables-persistent save"
echo " sudo /etc/init.d/iptables-persistent reload"
fi
createDir "$GITBUCKET_HOME"
createDir "$GITBUCKET_WAR_DIR"
createDir "$GITBUCKET_DIR"
createDir "$GITBUCKET_LOG_DIR"
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
echo "Copying gitbucket.conf to $GITBUCKET_DIR"
sudo cp gitbucket.conf $GITBUCKET_DIR
if [ `isUbuntu` ] || [ `isRedHat` ]; then
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
# Install gitbucket as a service that starts when system boots
sudo chown root:root $GITBUCKET_SERVICE
sudo chmod 755 $GITBUCKET_SERVICE
sudo update-rc.d "$(basename $GITBUCKET_SERVICE)" defaults 98 02
echo "Starting GitBucket service"
sudo $GITBUCKET_SERVICE start
elif [ `isMac` ]; then
sudo macosx/makePlist
echo "Starting GitBucket service"
sudo cp gitbucket.conf "$GITBUCKET_SERVICE"
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
sudo chmod a+x "$GITBUCKET_SERVICE"
sudo "$GITBUCKET_SERVICE" start
else
echo "Don't know how to install this OS"
exit -2
fi
if [ $? != 0 ]; then
less "$GITBUCKET_LOG_DIR/run.log"
fi

View File

@@ -1,3 +1,10 @@
#!/bin/bash
# From http://docstore.mik.ua/orelly/unix3/mac/ch02_02.htm
source gitbucket.conf
GITBUCKET_SERVICE_DIR=`dirname "$GITBUCKET_SERVICE"`
mkdir -p "$GITBUCKET_SERVICE_DIR"
cat << EOF > "$GITBUCKET_SERVICE_DIR/gitbucket.plist"
<?xml version="1.0" encoding="UTF-8"?> <?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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
@@ -7,14 +14,15 @@
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/usr/bin/java</string> <string>/usr/bin/java</string>
<string>-Dmail.smtp.starttls.enable=true</string> <string>$GITBUCKET_JVM_OPTS</string>
<string>-jar</string> <string>-jar</string>
<string>gitbucket.war</string> <string>gitbucket.war</string>
<string>--host=127.0.0.1</string> <string>--host=$GITBUCKET_HOST</string>
<string>--port=8080</string> <string>--port=$GITBUCKET_PORT</string>
<string>--https=true</string> <string>--https=true</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>
EOF

View File

@@ -1,17 +0,0 @@
# Bind host
#GITBUCKET_HOST=0.0.0.0
# Server port
#GITBUCKET_PORT=8080
# Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket
# Path to the WAR file
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
#GITBUCKET_PREFIX=
# Other Java option
#GITBUCKET_JVM_OPTS=

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

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

View File

@@ -1,55 +1,60 @@
import sbt._ import sbt._
import Keys._ import Keys._
import org.scalatra.sbt._ import org.scalatra.sbt._
import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
object MyBuild extends Build { object MyBuild extends Build {
val Organization = "jp.sf.amateras" val Organization = "jp.sf.amateras"
val Name = "gitbucket" val Name = "gitbucket"
val Version = "0.0.1" val Version = "0.0.1"
val ScalaVersion = "2.10.3" val ScalaVersion = "2.11.2"
val ScalatraVersion = "2.2.1" val ScalatraVersion = "2.3.0"
lazy val project = Project ( lazy val project = Project (
"gitbucket", "gitbucket",
file("."), file(".")
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
sourcesInBase := false,
organization := Organization,
name := Name,
version := Version,
scalaVersion := ScalaVersion,
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
),
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.5",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.14",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"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"),
"junit" % "junit" % "4.11" % "test"
),
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher")
) ++ seq(Twirl.settings: _*)
) )
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
.settings(
sourcesInBase := false,
organization := Organization,
name := Name,
version := Version,
scalaVersion := ScalaVersion,
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
),
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.10",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"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"),
"junit" % "junit" % "4.11" % "test",
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
),
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl)
} }

View File

@@ -4,8 +4,6 @@ addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5") addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
resolvers += "spray repo" at "http://repo.spray.io" addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")

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 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.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.5.jar" %*

3
sbt.sh
View File

@@ -1 +1,2 @@
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 "$@" #!/bin/sh
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.5.jar "$@"

View File

@@ -1,10 +1,8 @@
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.webapp.WebAppContext;
import java.io.IOException; import java.io.File;
import java.net.URL; import java.net.URL;
import java.security.ProtectionDomain; import java.security.ProtectionDomain;
@@ -44,6 +42,14 @@ public class JettyLauncher {
server.addConnector(connector); server.addConnector(connector);
WebAppContext context = new WebAppContext(); WebAppContext context = new WebAppContext();
File tmpDir = new File(getGitBucketHome(), "tmp");
if(tmpDir.exists()){
deleteDirectory(tmpDir);
}
tmpDir.mkdirs();
context.setTempDirectory(tmpDir);
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain(); ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation(); URL location = domain.getCodeSource().getLocation();
@@ -59,4 +65,27 @@ public class JettyLauncher {
server.start(); server.start();
server.join(); server.join();
} }
private static File getGitBucketHome(){
String home = System.getProperty("gitbucket.home");
if(home != null && home.length() > 0){
return new File(home);
}
home = System.getenv("GITBUCKET_HOME");
if(home != null && home.length() > 0){
return new File(home);
}
return new File(System.getProperty("user.home"), ".gitbucket");
}
private static void deleteDirectory(File dir){
for(File file: dir.listFiles()){
if(file.isFile()){
file.delete();
} else if(file.isDirectory()){
deleteDirectory(file);
}
}
dir.delete();
}
} }

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,6 @@
CREATE TABLE PLUGIN (
PLUGIN_ID VARCHAR(100) NOT NULL,
VERSION VARCHAR(100) NOT NULL
);
ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID);

View File

@@ -1,4 +1,4 @@
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._
@@ -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/*")

View File

@@ -5,6 +5,7 @@ import util._
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._
import ssh.SshUtil import ssh.SshUtil
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
@@ -291,7 +292,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){ LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
@@ -334,7 +335,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
builder.finish() builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit") Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
} }
} }
@@ -354,48 +355,43 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName){ if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
// redirect to the repository // redirect to the repository if repository already exists
redirect(s"/${repository.owner}/${repository.name}") redirect(s"/${loginUserName}/${repository.name}")
} else { } else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) => // Insert to the database at first
// redirect to the repository val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
redirect(s"/${owner}/${name}") val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository( createRepository(
repositoryName = repository.name, repositoryName = repository.name,
userName = loginUserName, userName = loginUserName,
description = repository.repository.description, description = repository.repository.description,
isPrivate = repository.repository.isPrivate, isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName), originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName), originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name), parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner) parentUserName = Some(repository.owner)
) )
// Insert default labels // Insert default labels
insertDefaultLabels(loginUserName, repository.name) insertDefaultLabels(loginUserName, repository.name)
// clone repository actually // clone repository actually
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name), getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name)) getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository // Create Wiki repository
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name), getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name)) getWikiRepositoryDir(loginUserName, repository.name))
// Record activity // Record activity
recordForkActivity(repository.owner, repository.name, loginUserName) recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository // redirect to the repository
redirect(s"/${loginUserName}/${repository.name}") redirect(s"/${loginUserName}/${repository.name}")
}
} }
} }
}) })

View File

@@ -9,7 +9,7 @@ import org.scalatra.json._
import org.json4s._ import org.json4s._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import model.Account import model._
import service.{SystemSettingsService, AccountService} import service.{SystemSettingsService, AccountService}
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import javax.servlet.{FilterChain, ServletResponse, ServletRequest} import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
@@ -24,8 +24,9 @@ abstract class ControllerBase extends ScalatraFilter
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
// Don't set content type via Accept header. // TODO Scala 2.11
override def format(implicit request: HttpServletRequest) = "" // // Don't set content type via Accept header.
// override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
val httpRequest = request.asInstanceOf[HttpServletRequest] val httpRequest = request.asInstanceOf[HttpServletRequest]
@@ -125,11 +126,13 @@ abstract class ControllerBase extends ScalatraFilter
} }
} }
override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty, // TODO Scala 2.11
includeContextPath: Boolean = true, includeServletPath: Boolean = true) override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
(implicit request: HttpServletRequest, response: HttpServletResponse) = includeContextPath: Boolean = true, includeServletPath: Boolean = true,
absolutize: Boolean = true, withSessionId: Boolean = true)
(implicit request: HttpServletRequest, response: HttpServletResponse): String =
if (path.startsWith("http")) path if (path.startsWith("http")) path
else baseUrl + url(path, params, false, false, false) else baseUrl + super.url(path, params, false, false, false)
} }

View File

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

View File

@@ -25,7 +25,9 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
post("/image/:owner/:repository"){ post("/image/:owner/:repository"){
execute { (file, fileId) => execute { (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(getAttachedDir(params("owner"), params("repository")), fileId), file.get) FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
} }
} }

View File

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

View File

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

View File

@@ -2,50 +2,67 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.CollaboratorsAuthenticator import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import util.Implicits._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator with LabelsService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService with RepositoryService with CollaboratorsAuthenticator => self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
val newForm = mapping( val labelForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color))) "labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
val editForm = mapping( get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), issues.labels.html.list(
"editColor" -> trim(label("Color", text(required, color))) getLabels(repository.owner, repository.name),
)(LabelForm.apply) countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) => hasWritePermission(repository.owner, repository.name, context.loginAccount))
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
}) })
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.edit(None, repository)
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
issues.labels.html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
issues.labels.html.edit(Some(label), repository) issues.labels.html.edit(Some(label), repository)
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1)) updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt) deleteLabel(repository.owner, repository.name, params("labelId").toInt)
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) Ok()
}) })
/** /**
@@ -53,7 +70,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 +79,4 @@ trait LabelsControllerBase extends ControllerBase {
} }
} }
} }

View File

@@ -13,7 +13,6 @@ import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._ import service.IssuesService._
import service.PullRequestService._ import service.PullRequestService._
import util.JGitUtil.DiffInfo import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
@@ -63,10 +62,6 @@ trait PullRequestsControllerBase extends ControllerBase {
searchPullRequests(None, repository) searchPullRequests(None, repository)
}) })
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -124,7 +119,7 @@ trait PullRequestsControllerBase extends ControllerBase {
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){ LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) => getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git => using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close. // mark issue as merged and close.
@@ -157,7 +152,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
mergeCommit.setAuthor(personIdent) mergeCommit.setAuthor(personIdent)
mergeCommit.setCommitter(personIdent) mergeCommit.setCommitter(personIdent)
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n\n" + mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" +
form.message) form.message)
// insertObject and got mergeCommit Object Id // insertObject and got mergeCommit Object Id
@@ -367,7 +362,7 @@ trait PullRequestsControllerBase extends ControllerBase {
*/ */
private def checkConflict(userName: String, repositoryName: String, branch: String, private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){ LockUtil.lock(s"${userName}/${repositoryName}"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}" val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}" val tmpRefName = s"refs/merge-check/${userName}/${branch}"
@@ -403,7 +398,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)
@@ -444,7 +439,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.commitTime) == view.helpers.date(commit2.commitTime)
} }
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
@@ -454,7 +449,6 @@ trait PullRequestsControllerBase extends ControllerBase {
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) =>
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName) val sessionKey = Keys.Session.Pulls(owner, repoName)
@@ -464,14 +458,15 @@ trait PullRequestsControllerBase extends ControllerBase {
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
) )
pulls.html.list( issues.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), "pulls",
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)), searchIssue(condition, Map.empty, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
userName,
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), (getCollaborators(owner, repoName) :+ owner).sorted,
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), getMilestones(owner, repoName),
countIssue(condition, Map.empty, true, owner -> repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), Map.empty, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), Map.empty, true, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

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

@@ -8,23 +8,31 @@ import _root_.util._
import service._ import service._
import org.scalatra._ import org.scalatra._
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.lib._ 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 jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} import org.eclipse.jgit.revwalk.RevCommit
import service.WebHookService.WebHookPayload
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator
/** /**
* The repository viewer. * The repository viewer.
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator => self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class EditorForm( case class EditorForm(
branch: String, branch: String,
@@ -32,6 +40,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
content: String, content: String,
message: Option[String], message: Option[String],
charset: String, charset: String,
lineSeparator: String,
newFileName: String, newFileName: String,
oldFileName: Option[String] oldFileName: Option[String]
) )
@@ -44,13 +53,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
) )
val editorForm = mapping( val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))), "branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())), "path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))), "content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))), "message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))), "charset" -> trim(label("Charset", text(required))),
"newFileName" -> trim(label("Filename", text(required))), "lineSeparator" -> trim(label("Line Separator", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text()))) "newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply) )(EditorForm.apply)
val deleteForm = mapping( val deleteForm = mapping(
@@ -67,7 +77,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html" contentType = "text/html"
view.helpers.markdown(params("content"), repository, view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean, params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean) params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
/** /**
@@ -101,8 +113,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext) }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound case Left(_) => NotFound
} }
} }
@@ -142,7 +154,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), None, form.content, form.charset, commitFile(repository, form.branch, form.path, Some(form.newFileName), None,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
form.message.getOrElse(s"Create ${form.newFileName}")) form.message.getOrElse(s"Create ${form.newFileName}"))
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${ redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
@@ -151,7 +164,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName, form.content, form.charset, commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
if(form.oldFileName.exists(_ == form.newFileName)){ if(form.oldFileName.exists(_ == form.newFileName)){
form.message.getOrElse(s"Update ${form.newFileName}") form.message.getOrElse(s"Update ${form.newFileName}")
} else { } else {
@@ -179,6 +193,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
getPathObjectId(git, path, revCommit).map { objectId => getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){ if(raw){
// Download // Download
@@ -188,7 +203,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
} else { } else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
} getOrElse NotFound } getOrElse NotFound
} }
@@ -226,6 +241,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
/**
* Creates a branch.
*/
post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
val newBranchName = params.getOrElse("new", halt(400))
val fromBranchName = params.getOrElse("from", halt(400))
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.createBranch(git, fromBranchName, newBranchName)
} match {
case Right(message) =>
flash += "info" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}")
case Left(message) =>
flash += "error" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
}
})
/** /**
* Deletes branch. * Deletes branch.
*/ */
@@ -252,50 +285,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Download repository contents as an archive. * Download repository contents as an archive.
*/ */
get("/:owner/:repository/archive/*")(referrersOnly { repository => get("/:owner/:repository/archive/*")(referrersOnly { repository =>
val name = multiParams("splat").head multiParams("splat").head match {
case name if name.endsWith(".zip") =>
if(name.endsWith(".zip")){ archiveRepository(name, ".zip", repository)
val revision = name.replaceFirst("\\.zip$", "") case name if name.endsWith(".tar.gz") =>
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) archiveRepository(name, ".tar.gz", repository)
if(workDir.exists){ case _ => BadRequest
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
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 =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new TreeWalk(git.getRepository)){ walk =>
val reader = walk.getObjectReader
val objectId = new MutableObjectId
using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
entry.setSize(loader.getSize)
out.putNextEntry(entry)
loader.copyTo(out)
}
}
}
}
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile
} else {
BadRequest
} }
}) })
@@ -316,9 +311,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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("/"))
} }
@@ -337,10 +332,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) 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)
// 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 =>
val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
// 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 val parentPath = if (path == ".") Nil else path.split("/").toList
@@ -355,8 +350,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount)) files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
flash.get("info"), flash.get("error"))
} }
} getOrElse NotFound } getOrElse NotFound
} }
@@ -376,7 +372,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val builder = DirCache.newInCore.builder() val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter() val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}" val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(s"refs/heads/${branch}") val headTip = git.getRepository.resolve(headName)
JGitUtil.processTree(git, headTip){ (path, tree) => JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){ if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
@@ -391,7 +387,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
builder.finish() builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter), val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, message) headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush() inserter.flush()
inserter.release() inserter.release()
@@ -408,8 +404,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
// TODO invoke hook // close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call web hook
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
}
case _ =>
}
} }
} }
} }
@@ -429,4 +436,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
} }
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
val file = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new java.io.FileOutputStream(file)) { out =>
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(out)
.call()
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
file
}
}
} }

View File

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

View File

@@ -3,8 +3,15 @@ package app
import service.{AccountService, SystemSettingsService} import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import ssh.SshServer import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
import org.scalatra.Ok
import util.Implicits._
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator with AccountService with AdminAuthenticator
@@ -36,8 +43,9 @@ trait SystemSettingsControllerBase extends ControllerBase {
"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))),
"additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))), "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", text(required))), "mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"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))
@@ -47,6 +55,11 @@ trait SystemSettingsControllerBase extends ControllerBase {
} else Nil } else Nil
} }
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
admin.html.system(flash.get("info")) admin.html.system(flash.get("info"))
@@ -71,4 +84,118 @@ trait SystemSettingsControllerBase extends ControllerBase {
redirect("/admin/system") redirect("/admin/system")
}) })
get("/admin/plugins")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
} else NotFound
})
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/available")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins)
} else NotFound
})
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
admin.plugins.html.console()
} else NotFound
})
post("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
val script = request.getParameter("script")
val result = plugin.ScalaPlugin.eval(script)
Ok()
} else NotFound
})
// 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.deleteDirectory(pluginDir)
}
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
} }

View File

@@ -4,10 +4,11 @@ import service._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import util.Directory._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.Directory._
class UserManagementController extends UserManagementControllerBase class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
@@ -48,7 +49,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
"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()))),
"clearImage" -> trim(label("Clear image" ,boolean())), "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean())) "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
@@ -181,11 +182,6 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
}) })
// TODO Move to other generic controller?
post("/admin/users/_usercheck"){
getAccountByUserName(params("userName")).isDefined
}
private def members: Constraint = new Constraint(){ private def members: 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.split(",").exists { if(value.split(",").exists {
@@ -194,4 +190,14 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
} }
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if(userName == context.loginAccount.get.userName)
Some("You can't disable your account yourself")
else
None
}
}
}
} }

View File

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

View File

@@ -1,34 +1,39 @@
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,
isAdmin: Boolean, case class Account(
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,25 +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( case class Activity(
activityId: Int,
userName: String, userName: String,
repositoryName: String, repositoryName: String,
activityUserName: String, activityUserName: String,
activityType: String, activityType: String,
message: String, message: String,
additionalInfo: Option[String], additionalInfo: Option[String],
activityDate: java.util.Date activityDate: java.util.Date,
activityId: Int = 0
) )

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 === owner.bind) && (repositoryName === 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 === userName) && (this.repositoryName === 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 === 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 === 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 === 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 === 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 === 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 === milestoneId)
}
}

View File

@@ -1,13 +1,17 @@
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 === collaborator.bind)
}
} }
case class Collaborator( case class Collaborator(

View File

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

View File

@@ -1,41 +1,49 @@
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],
title: String, case class Issue(
content: Option[String], userName: String,
closed: Boolean, repositoryName: String,
registeredDate: java.util.Date, issueId: Int,
updatedDate: java.util.Date, openedUserName: String,
isPullRequest: Boolean) milestoneId: Option[Int],
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 === commentId.bind
issueId: Int, }
commentId: Int, }
action: String,
commentedUserName: String, case class IssueComment(
content: String, userName: String,
registeredDate: java.util.Date, repositoryName: String,
updatedDate: java.util.Date issueId: Int,
) commentId: Int = 0,
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
)

View File

@@ -1,15 +1,20 @@
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 === labelId.bind)
}
} }
case class IssueLabel( case class IssueLabel(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
issueId: Int, issueId: Int,
labelId: Int) labelId: Int
)

View File

@@ -1,21 +1,25 @@
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)
def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
}
} }
case class Label( case class Label(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
labelId: Int, labelId: Int = 0,
labelName: String, labelName: String,
color: String){ color: String){
@@ -27,8 +31,7 @@ case class Label(
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000" "000000"
} else { } else {
"FFFFFF" "ffffff"
} }
} }
}
}

View File

@@ -1,24 +1,30 @@
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( case class Milestone(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
milestoneId: Int, milestoneId: Int = 0,
title: String, title: String,
description: Option[String], description: Option[String],
dueDate: Option[java.util.Date], dueDate: Option[java.util.Date],
closedDate: Option[java.util.Date]) closedDate: Option[java.util.Date]
)

View File

@@ -0,0 +1,19 @@
package model
trait PluginComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val Plugins = TableQuery[Plugins]
class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){
val pluginId = column[String]("PLUGIN_ID", O PrimaryKey)
val version = column[String]("VERSION")
def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply)
}
}
case class Plugin(
pluginId: String,
version: String
)

View File

@@ -0,0 +1,42 @@
package model
trait Profile {
val profile: slick.driver.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
}
}
object Profile extends {
val profile = slick.driver.H2Driver
} with AccountComponent
with ActivityComponent
with CollaboratorComponent
with GroupMemberComponent
with IssueComponent
with IssueCommentComponent
with IssueLabelComponent
with LabelComponent
with MilestoneComponent
with PullRequestComponent
with RepositoryComponent
with SshKeyComponent
with WebHookComponent
with PluginComponent with Profile {
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}

View File

@@ -1,18 +1,22 @@
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( case class PullRequest(
@@ -25,4 +29,4 @@ case class PullRequest(
requestBranch: String, requestBranch: String,
commitIdFrom: String, commitIdFrom: String,
commitIdTo: String commitIdTo: String
) )

View File

@@ -1,21 +1,26 @@
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( case class Repository(

View File

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

View File

@@ -1,12 +1,16 @@
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 === url.bind)
}
} }
case class WebHook( case class WebHook(

View File

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

View File

@@ -0,0 +1,22 @@
package plugin
import plugin.PluginSystem._
import java.sql.Connection
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[RepositoryAction]
def globalActions : List[Action]
def javaScripts : List[JavaScript]
}
object PluginConnectionHolder {
val threadLocal = new ThreadLocal[Connection]
}

View File

@@ -0,0 +1,194 @@
package plugin
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.{IOUtils, FileUtils}
import Security._
import service.PluginService
import model.Profile._
import profile.simple._
import java.io.FileInputStream
import java.sql.Connection
import app.Context
import service.RepositoryService.RepositoryInfo
/**
* Provides extension points to plug-ins.
*/
object PluginSystem extends PluginService {
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)(implicit session: Session): Unit = {
pluginsMap.remove(id)
// Delete from PLUGIN table
deletePlugin(id)
// Drop tables
val pluginDir = new java.io.File(PluginHome)
val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql")
if(sqlFile.exists){
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(session.conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
def repositories: List[PluginRepository] = repositoriesList.toList
/**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/
def init()(implicit session: Session): 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)(implicit session: Session): Unit = {
val pluginHome = new java.io.File(PluginHome)
val pluginDir = new java.io.File(pluginHome, id)
val scalaFile = new java.io.File(pluginDir, "plugin.scala")
if(scalaFile.exists && scalaFile.isFile){
val properties = new java.util.Properties()
using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
properties.load(in)
}
val pluginId = properties.getProperty("id")
val version = properties.getProperty("version")
val author = properties.getProperty("author")
val url = properties.getProperty("url")
val description = properties.getProperty("description")
val source = s"""
|val id = "${pluginId}"
|val version = "${version}"
|val author = "${author}"
|val url = "${url}"
|val description = "${description}"
""".stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8")
try {
// Compile and eval Scala source code
ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
ScalaPlugin.compileTemplate(
id.replaceAll("-", ""),
file.getName.replaceAll("\\.scala\\.html$", ""),
IOUtils.toString(new FileInputStream(file)))
}.mkString("\n") + source)
// Migrate database
val plugin = getPlugin(pluginId)
if(plugin.isEmpty){
registerPlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, "0.0")
} else {
updatePlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, plugin.get.version)
}
} catch {
case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
}
}
}
// TODO Should PluginSystem provide a way to migrate resources other than H2?
private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
val pluginDir = new java.io.File(PluginHome)
// TODO Is ot possible to use this migration system in GitBucket migration?
val dim = current.split("\\.")
val currentVersion = Version(dim(0).toInt, dim(1).toInt)
val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
if(sqlDir.exists && sqlDir.isDirectory){
sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
val array = file.getName.replaceFirst("\\.sql", "").split("_")
Version(array(0).toInt, array(1).toInt)
}
.sorted.reverse.takeWhile(_ > currentVersion)
.reverse.foreach { version =>
val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
}
case class Version(major: Int, minor: Int) extends Ordered[Version] {
override def compare(that: Version): Int = {
if(major != that.major){
major.compare(that.major)
} else{
minor.compare(that.minor)
}
}
def displayString: String = major + "." + minor
}
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).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(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any)
case class Button(label: String, href: String)
case class JavaScript(filter: String => Boolean, script: String)
/**
* 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
}
}
}

View File

@@ -0,0 +1,66 @@
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 = {
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,77 @@
package plugin
import scala.collection.mutable.ListBuffer
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import app.Context
import plugin.PluginSystem._
import plugin.PluginSystem.RepositoryMenu
import plugin.Security._
import service.RepositoryService.RepositoryInfo
import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
import play.twirl.compiler.TwirlCompiler
import scala.io.Codec
// 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[RepositoryAction]()
private val globalActionList = ListBuffer[Action]()
private val javaScriptList = ListBuffer[JavaScript]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def javaScripts : List[JavaScript] = javaScriptList.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(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
globalActionList += Action(method, path, security, function)
}
def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
repositoryActionList += RepositoryAction(method, path, security, function)
}
def addJavaScript(filter: String => Boolean, script: String): Unit = {
javaScriptList += JavaScript(filter, script)
}
}
object ScalaPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new ScalaPlugin(id, version, author, url, description)
def eval(source: String): Any = {
val toolbox = currentMirror.mkToolBox()
val tree = toolbox.parse(source)
toolbox.eval(tree)
}
def compileTemplate(packageName: String, name: String, source: String): String = {
val result = TwirlCompiler.parseAndGenerateCodeNewParser(
Array(packageName, name),
source.getBytes("UTF-8"),
Codec(scala.util.Properties.sourceEncoding),
"",
"play.twirl.api.HtmlFormat.Appendable",
"play.twirl.api.HtmlFormat",
"",
false)
result.replaceFirst("package .*", "")
}
}

View File

@@ -0,0 +1,36 @@
package plugin
/**
* Defines enum case classes to specify permission for actions which is provided by plugin.
*/
object Security {
sealed trait Security
/**
* All users and guests
*/
case class All() extends Security
/**
* Only signed-in users
*/
case class Login() extends Security
/**
* Only repository owner and collaborators
*/
case class Member() extends Security
/**
* Only repository owner and managers of group repository
*/
case class Owner() extends Security
/**
* Only administrators
*/
case class Admin() extends Security
}

View File

@@ -0,0 +1,56 @@
import java.sql.PreparedStatement
import play.twirl.api.Html
import util.ControlUtil._
import scala.collection.mutable.ListBuffer
package object plugin {
case class Redirect(path: String)
case class Fragment(html: Html)
case class RawData(contentType: String, content: Array[Byte])
object db {
// TODO labelled place holder support
def select(sql: String, params: Any*): Seq[Map[String, String]] = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
using(stmt.executeQuery()){ rs =>
val list = new ListBuffer[Map[String, String]]()
while(rs.next){
defining(rs.getMetaData){ meta =>
val map = Range(1, meta.getColumnCount + 1).map { i =>
val name = meta.getColumnName(i)
(name, rs.getString(name))
}.toMap
list += map
}
}
list
}
}
}
}
// TODO labelled place holder support
def update(sql: String, params: Any*): Int = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
stmt.executeUpdate()
}
}
}
private def setParams(stmt: PreparedStatement, params: Any*): Unit = {
params.zipWithIndex.foreach { case (p, i) =>
p match {
case x: String => stmt.setString(i + 1, x)
case x: Int => stmt.setInt(i + 1, x)
case x: Boolean => stmt.setBoolean(i + 1, x)
}
}
}
}
}

View File

@@ -1,13 +1,12 @@
package service package service
import model._ import model.Profile._
import scala.slick.driver.H2Driver.simple._ import profile.simple._
import Database.threadLocalSession import model.{Account, GroupMember}
// TODO [Slick 2.0]NOT import directly?
import model.Profile.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 +14,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 +24,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,17 +33,22 @@ trait AccountService {
/** /**
* Authenticate by LDAP. * Authenticate by LDAP.
*/ */
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String): Option[Account] = { private def ldapAuthentication(settings: SystemSettings, userName: String, password: String)
(implicit s: Session): Option[Account] = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match { LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(ldapUserInfo) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(ldapUserInfo.userName, true) match { getAccountByUserName(ldapUserInfo.userName, true) match {
case Some(x) if(!x.isRemoved) => { case Some(x) if(!x.isRemoved) => {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
} else {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
}
getAccountByUserName(ldapUserInfo.userName) getAccountByUserName(ldapUserInfo.userName)
} }
case Some(x) if(x.isRemoved) => { case Some(x) if(x.isRemoved) => {
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..") logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password) defaultAuthentication(userName, password)
} }
case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match { case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
@@ -53,7 +57,7 @@ trait AccountService {
getAccountByUserName(ldapUserInfo.userName) getAccountByUserName(ldapUserInfo.userName)
} }
case Some(x) if(x.isRemoved) => { case Some(x) if(x.isRemoved) => {
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..") logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password) defaultAuthentication(userName, password)
} }
case None => { case None => {
@@ -70,20 +74,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 === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] = def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === 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 === 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,
@@ -98,10 +103,10 @@ 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 === 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,
@@ -113,13 +118,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 === 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 === 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 = "",
@@ -134,35 +139,40 @@ 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 === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = { def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete GroupMembers.filter(_.groupName === groupName.bind).delete
members.foreach { case (userName, isManager) => members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager) GroupMembers insert GroupMember (groupName, userName, isManager)
} }
} }
def getGroupMembers(groupName: String): List[GroupMember] = def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] =
Query(GroupMembers) GroupMembers
.filter(_.groupName is groupName.bind) .filter(_.groupName === groupName.bind)
.sortBy(_.userName) .sortBy(_.userName)
.list .list
def getGroupsByUserName(userName: String): List[String] = def getGroupsByUserName(userName: String)(implicit s: Session): List[String] =
Query(GroupMembers) GroupMembers
.filter(_.userName is userName.bind) .filter(_.userName === 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 === userName.bind).delete
Query(Collaborators).filter(_.collaboratorName is userName.bind).delete Collaborators.filter(_.collaboratorName === userName.bind).delete
Query(Repositories).filter(_.userName is userName.bind).delete Repositories.filter(_.userName === userName.bind).delete
}
def getGroupNames(userName: String)(implicit s: Session): List[String] = {
List(userName) ++
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list
} }
} }
object AccountService extends AccountService object AccountService extends AccountService

View File

@@ -1,19 +1,19 @@
package service package service
import model._ import model.Profile._
import scala.slick.driver.H2Driver.simple._ import profile.simple._
import Database.threadLocalSession import model.Activity
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) =>
if(isPublic){ if(isPublic){
(t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind) (t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind)
} else { } else {
(t1.activityUserName is activityUserName.bind) (t1.activityUserName === activityUserName.bind)
} }
} }
.sortBy { case (t1, t2) => t1.activityId desc } .sortBy { case (t1, t2) => t1.activityId desc }
@@ -21,133 +21,154 @@ 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 === false.bind }
.sortBy { case (t1, t2) => t1.activityId desc } .sortBy { case (t1, t2) => t1.activityId desc }
.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 getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) }
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)
(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),

View File

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

View File

@@ -1,26 +1,32 @@
package service package service
import scala.slick.driver.H2Driver.simple._ import model.Profile._
import Database.threadLocalSession import profile.simple._
import model.Label
import model._
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): Int =
Labels.ins insert (owner, repository, labelName, color) Labels returning Labels.map(_.labelId) += 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,49 @@
package service package service
import scala.slick.driver.H2Driver.simple._ import model.Profile._
import Database.threadLocalSession import profile.simple._
import model.Milestone
import model._ // TODO [Slick 2.0]NOT import directly?
import model.Profile.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.? isDefined) }
.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 +51,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

@@ -0,0 +1,24 @@
package service
import model.Profile._
import profile.simple._
import model.Plugin
trait PluginService {
def getPlugins()(implicit s: Session): List[Plugin] =
Plugins.sortBy(_.pluginId).list
def registerPlugin(plugin: Plugin)(implicit s: Session): Unit =
Plugins.insert(plugin)
def updatePlugin(plugin: Plugin)(implicit s: Session): Unit =
Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version)
def deletePlugin(pluginId: String)(implicit s: Session): Unit =
Plugins.filter(_.pluginId === pluginId.bind).delete
def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] =
Plugins.filter(_.pluginId === pluginId.bind).firstOption
}

View File

@@ -1,44 +1,63 @@
package service package service
import scala.slick.driver.H2Driver.simple._ import model.Profile._
import Database.threadLocalSession import profile.simple._
import model._ import model.{PullRequest, Issue}
import util.ControlUtil._
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 updateCommitIdFrom(owner: String, repository: String, issueId: Int, commitIdFrom: String): Unit = def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdFrom).update(commitIdFrom) (implicit s: Session): List[PullRequestCount] =
PullRequests
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
Query(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 === closed.bind) &&
(t1.userName is owner.bind) && (t1.userName === owner.get.bind, owner.isDefined) &&
(t1.repositoryName is repository.get.bind, repository.isDefined) (t1.repositoryName === 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 getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
// PullRequests
// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
// .filter { case ((t1, t2), t3) =>
// (t2.closed === closed.bind) &&
// (
// (t3.isPrivate === false.bind) ||
// (t3.userName === userName.bind) ||
// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
// )
// }
// .groupBy { case ((t1, t2), t3) => t2.openedUserName }
// .map { case (userName, t) => userName -> t.length }
// .sortBy(_._2 desc)
// .list
// .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,
@@ -47,16 +66,17 @@ 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 === userName.bind) &&
(t1.requestRepositoryName is repositoryName.bind) && (t1.requestRepositoryName === repositoryName.bind) &&
(t1.requestBranch is branch.bind) && (t1.requestBranch === branch.bind) &&
(t2.closed is closed.bind) (t2.closed === closed.bind)
} }
.map { case (t1, t2) => t1 } .map { case (t1, t2) => t1 }
.list .list

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.Profile._
import profile.simple._
trait trait RepositorySearchService { self: IssuesService =>
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._ import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int = def countIssues(owner: String, repository: String, query: String)(implicit session: Session): Int =
searchIssuesByKeyword(owner, repository, query).length searchIssuesByKeyword(owner, repository, query).length
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] = def searchIssues(owner: String, repository: String, query: String)(implicit session: Session): List[IssueSearchResult] =
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) => searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult( IssueSearchResult(
issue.issueId, issue.issueId,
@@ -39,7 +38,7 @@ RepositorySearchService { self: IssuesService =>
Nil Nil
} else { } else {
val files = searchRepositoryFiles(git, query) val files = searchRepositoryFiles(git, query)
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD") val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD")
files.map { case (path, text) => files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query) val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult( FileSearchResult(
@@ -60,7 +59,7 @@ RepositorySearchService { self: IssuesService =>
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
val keywords = StringUtil.splitWords(query.toLowerCase) val keywords = StringUtil.splitWords(query.toLowerCase)
val list = new ListBuffer[(String, String)] val list = new scala.collection.mutable.ListBuffer[(String, String)]
while (treeWalk.next()) { while (treeWalk.next()) {
val mode = treeWalk.getFileMode(0) val mode = treeWalk.getFileMode(0)
@@ -108,7 +107,7 @@ object RepositorySearchService {
case class SearchResult( case class SearchResult(
files : List[(String, String)], files : List[(String, String)],
issues: List[(Issue, Int, String)]) issues: List[(model.Issue, Int, String)])
case class IssueSearchResult( case class IssueSearchResult(
issueId: Int, issueId: Int,

View File

@@ -1,8 +1,8 @@
package service package service
import model._ import model.Profile._
import scala.slick.driver.H2Driver.simple._ import profile.simple._
import Database.threadLocalSession import model.{Repository, Account, Collaborator}
import util.JGitUtil import util.JGitUtil
trait RepositoryService { self: AccountService => trait RepositoryService { self: AccountService =>
@@ -20,7 +20,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,66 +40,82 @@ trait RepositoryService { self: AccountService =>
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = { def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String)
(Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => (implicit s: Session): Unit = {
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) getAccountByUserName(newUserName).foreach { account =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t => Repositories.filter { t =>
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind) (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t => Repositories.filter { t =>
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind) (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t => PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
deleteRepository(oldUserName, oldRepositoryName) deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
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)) :_*)
Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
val updateActivities = Activities.filter { t => Issues.insertAll(issues.map { x => x.copy(
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || userName = newUserName,
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") repositoryName = newRepositoryName,
}.map { t => t.activityId ~ t.message }.list milestoneId = x.milestoneId.map { id =>
newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId
}
)} :_*)
updateActivities.foreach { case (activityId, message) => PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Activities.filter(_.activityId is activityId.bind).map(_.message).update( IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
message Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#") Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#") if(account.isGroupAccount){
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#") Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#") } 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 === 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): Unit = { def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete Activities .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
@@ -118,8 +135,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 === userName.bind) map (_.repositoryName) list
/** /**
* Returns the specified repository information. * Returns the specified repository information.
@@ -129,11 +146,11 @@ 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 === false.bind)
}.map(_.pullRequest).list }.map(_.pullRequest).list
new RepositoryInfo( new RepositoryInfo(
@@ -149,13 +166,35 @@ trait RepositoryService { self: AccountService =>
} }
} }
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = { /**
Query(Repositories).filter { t1 => * Returns the repositories without private repository that user does not have access right.
(t1.userName is userName.bind) || * Include public repository, private own repository and private but collaborator repository.
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists) *
* @param userName the user name of collaborator
* @return the repository infomation list
*/
def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = {
Repositories.filter { t1 =>
(t1.isPrivate === false.bind) ||
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).map{ t =>
(t.userName, t.repositoryName)
}.list
}
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === 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),
@@ -172,24 +211,32 @@ trait RepositoryService { self: AccountService =>
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = { def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories) case Some(x) if(x.isAdmin) => Repositories
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) || Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === 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 === x.userName.bind)} exists)
} }
// for Guests // for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind) case None => Repositories filter(_.isPrivate === false.bind)
}).filter { t => }).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE repositoryUserName.map { userName => t.userName === 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),
@@ -199,7 +246,7 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getRepositoryManagers(userName: String): Seq[String] = private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){ if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else { } else {
@@ -209,16 +256,16 @@ trait RepositoryService { self: AccountService =>
/** /**
* Updates the last activity date of the repository. * Updates the last activity date of the repository.
*/ */
def updateLastActivityDate(userName: String, repositoryName: String): Unit = def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate) Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
/** /**
* Save repository options. * Save repository options.
*/ */
def saveRepositoryOptions(userName: String, repositoryName: String, def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], defaultBranch: String, isPrivate: Boolean): Unit = description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)) Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => r.description.? ~ r.defaultBranch ~ r.isPrivate ~ r.updatedDate } .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
.update (description, defaultBranch, isPrivate, currentDate) .update (description, defaultBranch, isPrivate, currentDate)
/** /**
@@ -228,8 +275,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.
@@ -238,7 +285,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
/** /**
@@ -247,7 +294,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
/** /**
@@ -257,10 +304,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
@@ -269,17 +316,17 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getForkedCount(userName: String, repositoryName: String): Int = private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
Query(Repositories.filter { t => Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
}.length).first }.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] = def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
Query(Repositories).filter { t => Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
} }
.sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
} }

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
package service package service
import scala.slick.driver.H2Driver.simple._ import model.Profile._
import Database.threadLocalSession import profile.simple._
import model.{WebHook, Account}
import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
import util.JGitUtil import util.JGitUtil
@@ -12,7 +11,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 +18,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._
@@ -46,7 +44,7 @@ trait WebHookService {
val httpClient = HttpClientBuilder.create.build val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl => webHookURLs.foreach { webHookUrl =>
val f = future { val f = Future {
logger.debug(s"start web hook invocation for ${webHookUrl}") logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHookUrl.url)
@@ -87,23 +85,23 @@ object WebHookService {
refName, refName,
commits.map { commit => commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false) val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id
WebHookCommit( WebHookCommit(
id = commit.id, id = commit.id,
message = commit.fullMessage, message = commit.fullMessage,
timestamp = commit.time.toString, timestamp = commit.commitTime.toString,
url = commitUrl, url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath }, added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD && modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser( author = WebHookUser(
name = commit.committer, name = commit.committerName,
email = commit.mailAddress email = commit.committerEmailAddress
) )
) )
}.toList, },
WebHookRepository( WebHookRepository(
name = repositoryInfo.name, name = repositoryInfo.name,
url = repositoryInfo.httpUrl, url = repositoryInfo.httpUrl,

View File

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

View File

@@ -1,166 +1,251 @@
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
import service.SystemSettingsService
/**
* Version of GitBucket object AutoUpdate {
*
* @param majorVersion the major version /**
* @param minorVersion the minor version * Version of GitBucket
*/ *
case class Version(majorVersion: Int, minorVersion: Int){ * @param majorVersion the major version
* @param minorVersion the minor version
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version]) */
case class Version(majorVersion: Int, minorVersion: Int){
/**
* Execute update/MAJOR_MINOR.sql to update schema to this version. private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
* If corresponding SQL file does not exist, this method do nothing.
*/ /**
def update(conn: Connection): Unit = { * Execute update/MAJOR_MINOR.sql to update schema to this version.
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" * If corresponding SQL file does not exist, this method do nothing.
*/
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in => def update(conn: Connection): Unit = {
if(in != null){ val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
val sql = IOUtils.toString(in, "UTF-8")
using(conn.createStatement()){ stmt => using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
logger.debug(sqlPath + "=" + sql) if(in != null){
stmt.executeUpdate(sql) val sql = IOUtils.toString(in, "UTF-8")
} 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( /**
Version(1, 13), * The history of versions. A head of this sequence is the current BitBucket version.
Version(1, 12), */
Version(1, 11), val versions = Seq(
Version(1, 10), new Version(2, 5),
Version(1, 9), new Version(2, 4),
Version(1, 8), new Version(2, 3) {
Version(1, 7), override def update(conn: Connection): Unit = {
Version(1, 6), super.update(conn)
Version(1, 5), using(conn.createStatement.executeQuery("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'")){ rs =>
Version(1, 4), while(rs.next) {
new Version(1, 3){ val info = rs.getString("ADDITIONAL_INFO")
override def update(conn: Connection): Unit = { val newInfo = info.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
super.update(conn) if (info != newInfo) {
// Fix wiki repository configuration val id = rs.getString("ACTIVITY_ID")
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => using(conn.prepareStatement("UPDATE ACTIVITY SET ADDITIONAL_INFO=? WHERE ACTIVITY_ID=?")) { sql =>
while(rs.next){ sql.setString(1, newInfo)
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => sql.setLong(2, id.toLong)
defining(git.getRepository.getConfig){ config => sql.executeUpdate
if(!config.getBoolean("http", "receivepack", false)){ }
config.setBoolean("http", null, "receivepack", true) }
config.save }
} }
} FileUtils.deleteDirectory(Directory.getPluginCacheDir())
} FileUtils.deleteDirectory(new File(Directory.PluginHome))
} }
} },
} new Version(2, 2),
}, new Version(2, 1),
Version(1, 2), new Version(2, 0){
Version(1, 1), override def update(conn: Connection): Unit = {
Version(1, 0), import eu.medsea.mimeutil.{MimeUtil2, MimeType}
Version(0, 0)
) val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
/**
* The head version of BitBucket. super.update(conn)
*/ using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
val headVersion = versions.head while(rs.next){
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
/** if(dir.exists && dir.isDirectory){
* The version file (GITBUCKET_HOME/version). dir.listFiles.foreach { file =>
*/ if(file.getName.indexOf('.') < 0){
lazy val versionFile = new File(GitBucketHome, "version") 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)))
* Returns the current version from the version file. }
*/ }
def getCurrentVersion(): Version = { }
if(versionFile.exists){ }
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { }
case Array(majorVersion, minorVersion) => { }
versions.find { v => }
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt }
}.getOrElse(Version(0, 0)) },
} Version(1, 13),
case _ => Version(0, 0) Version(1, 12),
} Version(1, 11),
} else Version(0, 0) Version(1, 10),
} Version(1, 9),
Version(1, 8),
} Version(1, 7),
Version(1, 6),
/** Version(1, 5),
* Update database schema automatically in the context initializing. Version(1, 4),
*/ new Version(1, 3){
class AutoUpdateListener extends ServletContextListener { override def update(conn: Connection): Unit = {
import AutoUpdate._ super.update(conn)
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) // Fix wiki repository configuration
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
override def contextInitialized(event: ServletContextEvent): Unit = { while(rs.next){
val datadir = event.getServletContext.getInitParameter("gitbucket.home") using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
if(datadir != null){ defining(git.getRepository.getConfig){ config =>
System.setProperty("gitbucket.home", datadir) if(!config.getBoolean("http", "receivepack", false)){
} config.setBoolean("http", null, "receivepack", true)
org.h2.Driver.load() config.save
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") Version(1, 2),
} else if(!versions.contains(currentVersion)){ Version(1, 1),
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") Version(1, 0),
} else { Version(0, 0)
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}") * The head version of BitBucket.
} */
} val headVersion = versions.head
} catch {
case ex: Throwable => { /**
logger.error("Failed to schema update", ex) * The version file (GITBUCKET_HOME/version).
ex.printStackTrace() */
conn.rollback() lazy val versionFile = new File(GitBucketHome, "version")
}
} /**
} * Returns the current version from the version file.
logger.debug("End schema update") */
} def getCurrentVersion(): Version = {
if(versionFile.exists){
def contextDestroyed(sce: ServletContextEvent): Unit = { FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
// Nothing to do. case Array(majorVersion, minorVersion) => {
} versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
private def getConnection(servletContext: ServletContext): Connection = }.getOrElse(Version(0, 0))
DriverManager.getConnection( }
servletContext.getInitParameter("db.url"), case _ => Version(0, 0)
servletContext.getInitParameter("db.user"), }
servletContext.getInitParameter("db.password")) } else Version(0, 0)
}
}
}
/**
* Update database schema automatically in the context initializing.
*/
class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if(dataDir != null){
System.setProperty("gitbucket.home", dataDir)
}
org.h2.Driver.load()
val context = event.getServletContext
context.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
defining(getConnection(event.getServletContext)){ conn =>
logger.debug("Start schema update")
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")
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")
}
if(SystemSettingsService.enablePluginSystem){
getDatabase(context).withSession { implicit session =>
logger.debug("Starting plugin system...")
try {
plugin.PluginSystem.init()
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
} catch {
case ex: Throwable => {
logger.error("Failed to initialize plugin system", ex)
ex.printStackTrace()
throw ex
}
}
}
}
}
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"))
private def getDatabase(servletContext: ServletContext): scala.slick.jdbc.JdbcBackend.Database =
slick.jdbc.JdbcBackend.Database.forURL(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
}

View File

@@ -3,7 +3,7 @@ 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.Account import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
@@ -21,7 +21,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){
@@ -65,7 +65,8 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
} }
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Option[Account] = private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
(implicit session: Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match { authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case _ => None case _ => None

View File

@@ -17,6 +17,7 @@ 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 service.IssuesService.IssueSearchCondition
import model.Session
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -64,7 +65,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
@@ -76,14 +77,16 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "") val repository = paths(2).stripSuffix(".git")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
val hook = new CommitLogHook(owner, repository, pusher, baseUrl(request)) defining(request) { implicit r =>
receivePack.setPreReceiveHook(hook) val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPostReceiveHook(hook) receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
} }
receivePack receivePack
} }
@@ -92,7 +95,8 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook with PreReceiveHook class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: 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])
@@ -114,6 +118,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try { try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command => commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
@@ -133,10 +138,16 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
val newCommits = commits.flatMap { commit => val newCommits = commits.flatMap { commit =>
if (!existIds.contains(commit.id)) { if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) { if (issueCount > 0) {
pushedIds.add(commit.id)
createIssueComment(commit) createIssueComment(commit)
// close issues
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
}
} }
Some(commit) Some(commit)
} else None } else None
@@ -168,17 +179,6 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
} }
} }
// close issues
if(issueCount > 0) {
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if (refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE) {
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach {
commit =>
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
}
}
}
// call web hook // call web hook
getWebHookURLs(owner, repository) match { getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -205,7 +205,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
private def createIssueComment(commit: CommitInfo) = { private def createIssueComment(commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){ if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.mailAddress).foreach { account => getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
} }
} }
@@ -226,13 +226,10 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
.call .call
val commitIdTo = oldGit.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, val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.userName, pullreq.repositoryName, pullreq.branch,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch) pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
// TODO(tanacasino): commitIdFrom and commitIdTo should be updated by one query... updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
updateCommitIdFrom(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdFrom)
} }
} }
} }

View File

@@ -0,0 +1,192 @@
package servlet
import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils
import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.{Account, Session}
import util.{JGitUtil, Keys}
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
import service.RepositoryService.RepositoryInfo
import plugin.Security._
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 = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res)
}
}
}
}
}
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
plugin.PluginSystem.globalActions.find(x =>
x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path)
).map { action =>
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
if(authenticate(action.security, context)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
} getOrElse false
}
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
val elements = path.split("/")
if(elements.length > 3){
val owner = elements(1)
val name = elements(2)
val remain = elements.drop(3).mkString("/", "/", "")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
if(authenticate(action.security, context, repository)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context, repository)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
}
} getOrElse false
} else false
}
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
context: app.Context): Unit = {
result match {
case null|None => renderError(request, response, context, 404)
case x: String => renderGlobalHtml(request, response, context, x)
case Some(x: String) => renderGlobalHtml(request, response, context, x)
case x: Html => renderGlobalHtml(request, response, context, x.toString)
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
case x: RawData => renderRawData(request, response, context, x)
case Some(x: RawData) => renderRawData(request, response, context, x)
case x: Redirect => response.sendRedirect(x.path)
case Some(x: Redirect) => response.sendRedirect(x.path)
case x: AnyRef => renderJson(request, response, x)
}
}
/**
* Authentication for global action
*/
private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = {
// Global Action
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Admin() => context.loginAccount.exists(_.isAdmin)
case _ => false // TODO throw Exception?
}
}
/**
* Authenticate for repository action
*/
private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = {
if(repository.repository.isPrivate){
// Private Repository
security match {
case Admin() => context.loginAccount.exists(_.isAdmin)
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case _ => context.loginAccount.exists { account =>
account.isAdmin || account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
}
} else {
// Public Repository
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case Member() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
case Admin() => context.loginAccount.exists(_.isAdmin)
}
}
}
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
response.sendError(error)
}
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(Html(body))(context)
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
response.setContentType(rawData.contentType)
IOUtils.write(rawData.content, response.getOutputStream)
}
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
implicit val formats = Serialization.formats(NoTypeHints)
val json = write(obj)
response.setContentType("application/json; charset=UTF-8")
IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
}
}

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ object SshUtil {
// TODO RFC 4716 Public Key is not supported... // TODO RFC 4716 Public Key is not supported...
val parts = key.split(" ") val parts = key.split(" ")
if (parts.size < 2) { if (parts.size < 2) {
logger.debug(s"Invalid PublicKey Format: key") logger.debug(s"Invalid PublicKey Format: ${key}")
return None return None
} }
try { try {

View File

@@ -34,6 +34,10 @@ object Directory {
val DatabaseHome = s"${GitBucketHome}/data" val DatabaseHome = s"${GitBucketHome}/data"
val PluginHome = s"${GitBucketHome}/plugins"
val TemporaryHome = s"${GitBucketHome}/tmp"
/** /**
* Substance directory of the repository. * Substance directory of the repository.
*/ */
@@ -55,13 +59,18 @@ object Directory {
* Root of temporary directories for the upload file. * Root of temporary directories for the upload file.
*/ */
def getTemporaryDir(sessionId: String): File = def getTemporaryDir(sessionId: String): File =
new File(s"${GitBucketHome}/tmp/_upload/${sessionId}") 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

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

View File

@@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git
import util.Directory._ import util.Directory._
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import scala.annotation.tailrec
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._ import org.eclipse.jgit.revwalk._
@@ -13,7 +14,7 @@ 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.{ConfigInvalidException, 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.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
import service.RepositoryService import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -35,7 +36,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.
@@ -43,38 +48,45 @@ object JGitUtil {
* @param id the object id * @param id the object id
* @param isDirectory whether is it directory * @param isDirectory whether is it directory
* @param name the file (or directory) name * @param name the file (or directory) name
* @param time the last modified time
* @param message the last commit message * @param message the last commit message
* @param commitId the last commit id * @param commitId the last commit id
* @param committer the last committer name * @param time the last modified time
* @param author the last committer name
* @param mailAddress the committer's mail address * @param mailAddress the committer's mail address
* @param linkUrl the url of submodule * @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, message: String, commitId: String,
committer: String, mailAddress: String, linkUrl: Option[String]) time: Date, author: String, mailAddress: String, linkUrl: Option[String])
/** /**
* The commit data. * The commit data.
* *
* @param id the commit id * @param id the commit id
* @param time the commit time
* @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message * @param shortMessage the short message
* @param fullMessage the full message * @param fullMessage the full message
* @param parents the list of parent commit id * @param parents the list of parent commit id
* @param authorTime the author time
* @param authorName the author name
* @param authorEmailAddress the mail address of the author
* @param commitTime the commit time
* @param committerName the committer name
* @param committerEmailAddress the mail address of the committer
*/ */
case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String, case class CommitInfo(id: String, shortMessage: String, fullMessage: String, parents: List[String],
shortMessage: String, fullMessage: String, parents: List[String]){ authorTime: Date, authorName: String, authorEmailAddress: String,
commitTime: Date, committerName: String, committerEmailAddress: String){
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
rev.getName, rev.getName,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress,
rev.getShortMessage, rev.getShortMessage,
rev.getFullMessage, rev.getFullMessage,
rev.getParents().map(_.name).toList) rev.getParents().map(_.name).toList,
rev.getAuthorIdent.getWhen,
rev.getAuthorIdent.getName,
rev.getAuthorIdent.getEmailAddress,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress)
val summary = getSummaryMessage(fullMessage, shortMessage) val summary = getSummaryMessage(fullMessage, shortMessage)
@@ -83,6 +95,8 @@ object JGitUtil {
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} else None } else None
} }
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
} }
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -94,7 +108,12 @@ object JGitUtil {
* @param content the string content * @param content the string content
* @param charset the character encoding * @param charset the character encoding
*/ */
case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]) case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]){
/**
* the line separator of this content ("LF" or "CRLF")
*/
val lineSeparator: String = if(content.exists(_.indexOf("\r\n") >= 0)) "CRLF" else "LF"
}
/** /**
* The tag data. * The tag data.
@@ -146,12 +165,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 {
@@ -172,38 +191,23 @@ 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, Option[String])] var 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)
val revCommit = revWalk.parseCommit(objectId) val revCommit = revWalk.parseCommit(objectId)
using(new TreeWalk(git.getRepository)){ treeWalk => val treeWalk = if (path == ".") {
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
if(path != "."){ treeWalk
treeWalk.setRecursive(true) } else {
treeWalk.setFilter(new TreeFilter(){ val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
treeWalk.enterSubtree()
treeWalk
}
var stopRecursive = false using(treeWalk) { treeWalk =>
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
while (treeWalk.next()) { while (treeWalk.next()) {
// submodule // submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
@@ -212,6 +216,31 @@ object JGitUtil {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
} }
list = list.map(tuple =>
if (tuple._2 != FileMode.TREE)
tuple
else
simplifyPath(tuple)
)
@tailrec
def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(tuple._1)
while (walk.next() && list.size < 2) {
val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url)
} else None
list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl))
}
}
if (list.size != 1 || list.exists(_._2 != FileMode.TREE))
tuple
else
simplifyPath(list(0))
}
} }
} }
@@ -222,11 +251,11 @@ object JGitUtil {
objectId, objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name, name,
commit.getCommitterIdent.getWhen,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage), getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName, commit.getName,
commit.getCommitterIdent.getName, commit.getAuthorIdent.getWhen,
commit.getCommitterIdent.getEmailAddress, commit.getAuthorIdent.getName,
commit.getAuthorIdent.getEmailAddress,
linkUrl) linkUrl)
} }
}.sortWith { (file1, file2) => }.sortWith { (file1, file2) =>
@@ -372,7 +401,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 {
@@ -473,6 +507,17 @@ object JGitUtil {
}.find(_._1 != null) }.find(_._1 != null)
} }
def createBranch(git: Git, fromBranch: String, newBranch: String) = {
try {
git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call()
Right("Branch created.")
} catch {
case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.")
// JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists.
case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.")
}
}
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = { def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path) val entry = new DirCacheEntry(path)
entry.setFileMode(mode) entry.setFileMode(mode)
@@ -481,7 +526,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): ObjectId = { ref: 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))
@@ -495,7 +540,7 @@ object JGitUtil {
inserter.flush() inserter.flush()
inserter.release() inserter.release()
val refUpdate = git.getRepository.updateRef(Constants.HEAD) val refUpdate = git.getRepository.updateRef(ref)
refUpdate.setNewObjectId(newHeadId) refUpdate.setNewObjectId(newHeadId)
refUpdate.update() refUpdate.update()
@@ -629,4 +674,15 @@ object JGitUtil {
}.head.id }.head.id
} }
/**
* Returns the last modified commit of specified path
* @param git the Git object
* @param startCommit the search base commit id
* @param path the path of target file or directory
* @return the last modified commit of specified path
*/
def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = {
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
}
} }

View File

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

View File

@@ -7,6 +7,7 @@ import java.security.Security
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import service.SystemSettingsService.Ldap import service.SystemSettingsService.Ldap
import scala.annotation.tailrec import scala.annotation.tailrec
import model.Account
/** /**
* Utility for LDAP authentication. * Utility for LDAP authentication.
@@ -16,6 +17,26 @@ object LDAPUtil {
private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3 private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
private val logger = LoggerFactory.getLogger(getClass().getName()) private val logger = LoggerFactory.getLogger(getClass().getName())
private val LDAP_DUMMY_MAL = "@ldap-devnull"
/**
* Returns true if mail address ends with "@ldap-devnull"
*/
def isDummyMailAddress(account: Account): Boolean = {
account.mailAddress.endsWith(LDAP_DUMMY_MAL)
}
/**
* Creates dummy address (userName@ldap-devnull) for LDAP login.
*
* If mail address is not managed in LDAP server, GitBucket stores this dummy address in first LDAP login.
* GitBucket does not send any mails to this dummy address. And these users must input their mail address
* at the first step after LDAP authentication.
*/
def createDummyMailAddress(userName: String): String = {
userName + LDAP_DUMMY_MAL
}
/** /**
* Try authentication by LDAP using given configuration. * Try authentication by LDAP using given configuration.
* Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage). * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
@@ -30,7 +51,7 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""), keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed." error = "System LDAP authentication failed."
){ conn => ){ conn =>
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match { findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute, ldapSettings.additionalFilterCondition) match {
case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password) case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
case None => Left("User does not exist.") case None => Left("User does not exist.")
} }
@@ -47,14 +68,23 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""), keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed." error = "User LDAP Authentication Failed."
){ conn => ){ conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { if(ldapSettings.mailAttribute.getOrElse("").isEmpty) {
case Some(mailAddress) => Right(LDAPUserInfo( Right(LDAPUserInfo(
userName = getUserNameFromMailAddress(userName), userName = userName,
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, fullNameAttribute) findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}.getOrElse(userName), }.getOrElse(userName),
mailAddress = mailAddress)) mailAddress = createDummyMailAddress(userName)))
case None => Left("Can't find mail address.") } else {
findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute.get) match {
case Some(mailAddress) => Right(LDAPUserInfo(
userName = getUserNameFromMailAddress(userName),
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}.getOrElse(userName),
mailAddress = mailAddress))
case None => Left("Can't find mail address.")
}
} }
} }
} }
@@ -112,7 +142,7 @@ object LDAPUtil {
/** /**
* Search a specified user and returns userDN if exists. * Search a specified user and returns userDN if exists.
*/ */
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = { private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String, additionalFilterCondition: Option[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] = {
if(results.hasMore){ if(results.hasMore){
@@ -125,20 +155,26 @@ object LDAPUtil {
entries.flatten entries.flatten
} }
} }
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)).collectFirst {
val filterCond = additionalFilterCondition.getOrElse("") match {
case "" => userNameAttribute + "=" + userName
case x => "(&(" + x + ")(" + userNameAttribute + "=" + userName + "))"
}
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, filterCond, null, false)).collectFirst {
case x => x.getDN case x => x.getDN
} }
} }
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None } else None
} }
private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] = private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None } else None

View File

@@ -1,116 +1,116 @@
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 model.Session
import servlet.Database import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import SystemSettingsService.Smtp import servlet.Database
import _root_.util.ControlUtil.defining import SystemSettingsService.Smtp
import _root_.util.ControlUtil.defining
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) filterNot (LDAPUtil.isDummyMailAddress(_)) 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 { database withSession { implicit session =>
getIssue(r.owner, r.name, issueId.toString) foreach { issue => getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining( defining(
s"[${r.name}] ${issue.title} (#${issueId})" -> s"[${r.name}] ${issue.title} (#${issueId})" ->
msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
recipients(issue) { to => recipients(issue) { to =>
val email = new HtmlEmail val email = new HtmlEmail
email.setHostName(smtp.host) email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get) email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user => smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
} }
smtp.ssl.foreach { ssl => smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl) email.setSSLOnConnect(ssl)
} }
smtp.fromAddress smtp.fromAddress
.map (_ -> smtp.fromName.orNull) .map (_ -> smtp.fromName.orNull)
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) => .foreach { case (address, name) =>
email.setFrom(address, name) email.setFrom(address, name)
} }
email.setCharset("UTF-8") email.setCharset("UTF-8")
email.setSubject(subject) email.setSubject(subject)
email.setHtmlMsg(msg) email.setHtmlMsg(msg)
email.addTo(to).send email.addTo(to).send
} }
} }
} }
} }
"Notifications Successful." "Notifications Successful."
} }
f onSuccess { f onSuccess {
case s => logger.debug(s) case s => logger.debug(s)
} }
f onFailure { f onFailure {
case t => logger.error("Notifications Failed.", t) case t => logger.error("Notifications Failed.", t)
} }
} }
} }
class MockMailer extends Notifier { class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit = {} (msg: String => String)(implicit context: Context): Unit = {}
} }

View File

@@ -46,6 +46,22 @@ object StringUtil {
} }
} }
/**
* Converts line separator in the given content.
*
* @param content the content
* @param lineSeparator "LF" or "CRLF"
* @return the converted content
*/
def convertLineSeparator(content: String, lineSeparator: String): String = {
val lf = content.replace("\r\n", "\n").replace("\r", "\n")
if(lineSeparator == "CRLF"){
lf.replace("\n", "\r\n")
} else {
lf
}
}
/** /**
* Extract issue id like ```#issueId``` from the given message. * Extract issue id like ```#issueId``` from the given message.
* *

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

@@ -1,7 +1,7 @@
package view package view
import service.RequestCache import service.RequestCache
import twirl.api.Html import play.twirl.api.Html
import util.StringUtil import util.StringUtil
trait AvatarImageProvider { self: RequestCache => trait AvatarImageProvider { self: RequestCache =>

View File

@@ -9,6 +9,7 @@ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer import java.text.Normalizer
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.{RequestCache, WikiService} import service.{RequestCache, WikiService}
@@ -18,17 +19,23 @@ object Markdown {
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { enableWikiLink: Boolean, enableRefsLink: Boolean,
enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = {
// escape issue id // escape issue id
val source = if(enableRefsLink){ val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown } else markdown
// escape task list
val source = if(enableTaskList){
GitBucketHtmlSerializer.escapeTaskList(s)
} else s
val rootNode = new PegDownProcessor( val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
).parseMarkdown(source.toCharArray) ).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode) new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
} }
} }
@@ -45,7 +52,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
(text, text) (text, text)
} }
val url = repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){ if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label) new Rendering(url, label)
@@ -82,14 +89,18 @@ class GitBucketHtmlSerializer(
markdown: String, markdown: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableWikiLink: Boolean,
enableRefsLink: Boolean enableRefsLink: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean
)(implicit val context: app.Context) extends ToHtmlSerializer( )(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink), new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache { ) 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, true)).print("\">")
.print("<img src=\"").print(fixUrl(url, true)).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')
@@ -100,11 +111,23 @@ class GitBucketHtmlSerializer(
printer.print('>').print(rendering.text).print("</a>") printer.print('>').print(rendering.text).print("</a>")
} }
private def fixUrl(url: String): String = { private def fixUrl(url: String, isImage: Boolean = false): String = {
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")){ if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
url url
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
} else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
} else {
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
}
} else { } else {
repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
} }
} }
@@ -129,7 +152,10 @@ class GitBucketHtmlSerializer(
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 t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
// convert task list to checkbox.
val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t
if (abbreviations.isEmpty) { if (abbreviations.isEmpty) {
printer.print(text) printer.print(text)
@@ -137,6 +163,28 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text) printWithAbbreviations(text)
} }
} }
override def visit(node: BulletListNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println().print("""<ul class="task-list">""").indent(+2)
visitChildren(node)
printer.indent(-2).println().print("</ul>")
} else {
printIndentedTag(node, "ul")
}
}
override def visit(node: ListItemNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println()
printer.print("""<li class="task-list-item">""")
visitChildren(node)
printer.print("</li>")
} else {
printer.println()
printTag(node, "li")
}
}
} }
object GitBucketHtmlSerializer { object GitBucketHtmlSerializer {
@@ -149,4 +197,14 @@ object GitBucketHtmlSerializer {
val noSpecialChars = StringUtil.urlEncode(normalized) val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH) noSpecialChars.toLowerCase(Locale.ENGLISH)
} }
def escapeTaskList(text: String): String = {
Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
}
def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
val disabled = if (hasWritePermission) "" else "disabled"
text.replaceAll("task:x:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
}
} }

View File

@@ -1,7 +1,7 @@
package view package view
import java.util.Date import java.util.{Locale, Date, TimeZone}
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import twirl.api.Html import play.twirl.api.Html
import util.StringUtil import util.StringUtil
import service.RequestCache import service.RequestCache
@@ -15,10 +15,55 @@ 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)
val timeUnits = List(
(1000L, "second"),
(1000L * 60, "minute"),
(1000L * 60 * 60, "hour"),
(1000L * 60 * 60 * 24, "day"),
(1000L * 60 * 60 * 24 * 30, "month"),
(1000L * 60 * 60 * 24 * 365, "year")
).reverse
/**
* Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago"
*/
def datetimeAgo(date: Date): String = {
val duration = new Date().getTime - date.getTime
timeUnits.find(tuple => duration / tuple._1 > 0) match {
case Some((unitValue, unitString)) =>
val value = duration / unitValue
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
case None => "just now"
}
}
/**
*
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
* If duration over 1 month, format to "d MMM (yyyy)"
*
*/
def datetimeAgoRecentOnly(date: Date): String = {
val duration = new Date().getTime - date.getTime
timeUnits.find(tuple => duration / tuple._1 > 0) match {
case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
case Some((unitValue, unitString)) =>
val value = duration / unitValue
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
case None => "just now"
}
}
/** /**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". * 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") def datetimeRFC3339(date: Date): String = {
val sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
sf.setTimeZone(TimeZone.getTimeZone("UTC"))
sf.format(date)
}
/** /**
* Format java.util.Date to "yyyy-MM-dd". * Format java.util.Date to "yyyy-MM-dd".
@@ -44,8 +89,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
def renderMarkup(filePath: List[String], fileContent: String, branch: String, def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
@@ -74,7 +119,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* 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.
*/ */
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html = def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
getAvatarImageHtml(commit.committer, size, commit.mailAddress) getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress)
/** /**
* Converts commit id, issue id and username to the link. * Converts commit id, issue id and username to the link.
@@ -196,7 +241,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
case x if(x.endsWith(".sql")) => "sql" case x if(x.endsWith(".sql")) => "sql"
case x if(x.endsWith(".tcl")) => "tcl" case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".vbs")) => "vbscript" case x if(x.endsWith(".vbs")) => "vbscript"
case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".yml")) => "yaml" case x if(x.endsWith(".yml")) => "yaml"
case _ => "plain_text" case _ => "plain_text"
} }

View File

@@ -1,61 +1,65 @@
@(account: 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._
@import util.LDAPUtil
@html.main("Edit your profile"){ @html.main("Edit your profile"){
<div class="row-fluid"> <div class="container">
<div class="span3"> <div class="row-fluid">
@menu("profile", settings.ssh) <div class="span3">
</div> @menu("profile", settings.ssh)
<div class="span9"> </div>
@helper.html.information(info) <div class="span9">
<form action="@url(account.userName)/_edit" method="POST" validate="true"> @helper.html.information(info)
<div class="box"> @if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
<div class="box-header">Profile</div> <form action="@url(account.userName)/_edit" method="POST" validate="true">
<div class="box-content"> <div class="box">
<div class="row-fluid"> <div class="box-header">Profile</div>
<div class="span6"> <div class="box-content">
@if(account.password.nonEmpty){ <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> <fieldset>
<label for="password" class="strong"> <label for="fullName" class="strong">Full Name:</label>
Password (input to change password): <input type="text" name="fullName" id="fullName" value="@account.fullName"/>
</label> <span id="error-fullName" class="error"></span>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset> </fieldset>
} <fieldset>
<fieldset> <label for="mailAddress" class="strong">Mail Address:</label>
<label for="fullName" class="strong">Full Name:</label> <input type="text" name="mailAddress" id="mailAddress" value="@if(!LDAPUtil.isDummyMailAddress(account)){@account.mailAddress}"/>
<input type="text" name="fullName" id="fullName" value="@account.fullName"/> <span id="error-mailAddress" class="error"></span>
<span id="error-fullName" class="error"></span> </fieldset>
</fieldset> <fieldset>
<fieldset> <label for="url" class="strong">URL (optional):</label>
<label for="mailAddress" class="strong">Mail Address:</label> <input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/>
<input type="text" name="mailAddress" id="mailAddress" value="@account.mailAddress"/> <span id="error-url" class="error"></span>
<span id="error-mailAddress" class="error"></span> </fieldset>
</fieldset> </div>
<fieldset> <div class="span6">
<label for="url" class="strong">URL (optional):</label> <fieldset>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/> <label for="avatar" class="strong">Image (optional):</label>
<span id="error-url" class="error"></span> @helper.html.uploadavatar(Some(account))
</fieldset> </fieldset>
</div>
</div> </div>
<div class="span6"> <div style="margin-top: 20px;">
<fieldset> <div class="pull-right">
<label for="avatar" class="strong">Image (optional):</label> <a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
@helper.html.uploadavatar(Some(account)) </div>
</fieldset> <input type="submit" class="btn btn-success" value="Save"/>
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn">Cancel</a>}
</div> </div>
</div> </div>
<div style="margin-top: 20px;">
<div class="pull-right">
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div>
<input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.userName)" class="btn">Cancel</a>
</div>
</div> </div>
</div> </form>
</form> </div>
</div> </div>
} }
<script> <script>
@@ -64,4 +68,4 @@ $(function(){
return confirm('Once you delete your account, there is no going back.\nAre you sure?'); return confirm('Once you delete your account, there is no going back.\nAre you sure?');
}); });
}); });
</script> </script>

View File

@@ -2,7 +2,7 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(if(account.isEmpty) "Create group" else "Edit group"){ @html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div> <div class="container">
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true"> <form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span5"> <div class="span5">
@@ -60,7 +60,7 @@ $(function(){
}); });
$('#addMember').click(function(){ $('#addMember').click(function(){
$('#error-memberName').text(''); $('#error-members').text('');
var userName = $('#memberName').val(); var userName = $('#memberName').val();
// check empty // check empty
@@ -73,18 +73,18 @@ $(function(){
return $(this).data('name') == userName; return $(this).data('name') == userName;
}).length > 0; }).length > 0;
if(exists){ if(exists){
$('#error-memberName').text('User has been already added.'); $('#error-members').text('User has been already added.');
return false; return false;
} }
// check existence // check existence
$.post('@path/admin/users/_usercheck', { $.post('@path/_user/existence', {
'userName': userName 'userName': userName
}, function(data, status){ }, function(data, status){
if(data == 'true'){ if(data == 'true'){
addMemberHTML(userName, false); addMemberHTML(userName, false);
} else { } else {
$('#error-memberName').text('User does not exist.'); $('#error-members').text('User does not exist.');
} }
}); });
}); });

View File

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

View File

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

View File

@@ -25,7 +25,7 @@
@if(repository.repository.description.isDefined){ @if(repository.repository.description.isDefined){
<div>@repository.repository.description</div> <div>@repository.repository.description</div>
} }
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div> <div><span class="muted small">Updated @helper.html.datetimeago(repository.repository.lastActivityDate)</span></div>
</div> </div>
</div> </div>
} }

View File

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

View File

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

View File

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

View File

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

View File

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

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