Compare commits

..

276 Commits
2.1 ... 2.7

Author SHA1 Message Date
Naoki Takezoe
9d56d72611 Update README.md for 2.7 release 2014-12-29 11:32:23 +09:00
Naoki Takezoe
527c91ff9d Fix for #581. Column name and CSS is changed. 2014-12-29 03:24:04 +09:00
Naoki Takezoe
c58c2d6700 Merge pull request #556 from michaeljayt/add-ssh-clone-url
Add SSH clone url option when enabled SSH access
2014-12-29 02:12:18 +09:00
Naoki Takezoe
5518eca952 Merge pull request #581 from mrkm4ntr/fix-comment-in-pr
Fix bug on showing inline comments in Pull Request.
2014-12-29 02:10:57 +09:00
Shintaro Murakami
6e2b67ec0b Fix bug on showing inline comments in Pull Request. 2014-12-28 20:12:32 +09:00
Naoki Takezoe
837b1e44a7 Merge pull request #561 from torutk/rpm_nonroot
Enable contrib init file and RPM's spec file to run on RHEL 6/CentOS 6 a...
2014-12-27 20:55:26 +09:00
Naoki Takezoe
e04c230c6e Merge pull request #580 from HairyFotr/patch-lint
Fix a few issues detected by static analysis
2014-12-27 20:10:02 +09:00
HairyFotr
a01b5a4a59 Fix a few issues detected by static analysis 2014-12-26 15:40:05 +01:00
Naoki Takezoe
427b6ce846 Merge pull request #579 from banjun/fix-editor-preview-NoSuchElementException-enableTaskList
fix repo editor fails to preview
2014-12-22 23:06:46 +09:00
banjun
b7b5af2b72 add enableTaskList to post params for _preview 2014-12-22 19:36:12 +09:00
Naoki Takezoe
39fec57f72 (refs #578)Add migration for repositories which have removed parent or origin repository. 2014-12-22 01:32:15 +09:00
Naoki Takezoe
238dedb6df (refs #578)Clean original and parent repository information when it's deleted 2014-12-21 21:39:20 +09:00
Naoki Takezoe
af091117b7 (refs #577)Remove all HTML tags in Markdown 2014-12-20 01:51:15 +09:00
michaeljayt
ddea4e12f0 Add SSH clone url option when enabled SSH access 2014-12-12 21:58:21 +08:00
Naoki Takezoe
9767903252 (refs #567)Fix condition of repository search for issues. 2014-12-05 02:10:28 +09:00
takezoe
bc75f9f8a2 (refs #564)Fix for repository renaming 2014-11-28 01:42:44 +09:00
takezoe
63627fc1d0 (refs #564)Change the attached files directory to /commens from /issues 2014-11-28 00:20:46 +09:00
Naoki Takezoe
c23985c1a7 Merge pull request #564 from mrkm4ntr/coment-for-diff
(refs #9) Comments for commit and diff
2014-11-28 00:08:59 +09:00
Shintaro Murakami
af58e99dcf (refs #9) Comments for commit and diff 2014-11-26 22:59:52 +09:00
Toru Takahashi
676670e9e3 Enable contrib init file and RPM's spec file to run on RHEL 6/CentOS 6 as non root user. 2014-11-24 11:51:36 +09:00
Naoki Takezoe
823c52e941 Update README.md 2014-11-24 03:18:10 +09:00
Naoki Takezoe
7f42007648 (refs #507)Small fix about pull request UI 2014-11-23 14:59:03 +09:00
Naoki Takezoe
7214ef21d2 (refs #559)Fix merged message 2014-11-23 11:45:22 +09:00
Naoki Takezoe
18a4492975 Update README.md 2014-11-23 11:19:24 +09:00
Naoki Takezoe
99f73b1016 (refs #560)Replace build status badge with Travis 2014-11-23 01:29:38 +09:00
Naoki Takezoe
0c1ce6a088 (refs #560)Add travis configuration 2014-11-23 01:19:26 +09:00
Naoki Takezoe
ae6291ab83 Update version to 2.6 2014-11-22 22:36:27 +09:00
Naoki Takezoe
617fcf7c99 Fix compilation error in test 2014-11-22 22:34:27 +09:00
Naoki Takezoe
9df4a74837 (refs #507)Applying new UI to pull request detail page has been completed. 2014-11-22 22:30:44 +09:00
Naoki Takezoe
966d4251be (refs #507)Applying new UI to pull request detail page 2014-11-22 22:02:33 +09:00
Naoki Takezoe
84b2e9cdcd Fix compilation error 2014-11-22 21:47:34 +09:00
Naoki Takezoe
e29d63c91a Merge branch 'master' into newui-for-pullreq 2014-11-22 21:46:51 +09:00
Naoki Takezoe
805d2b8e79 (refs #530)Don't re-sort activities by repository renaming. 2014-11-22 21:40:32 +09:00
Naoki Takezoe
9983fd1292 Update README.md 2014-11-22 12:57:33 +09:00
Naoki Takezoe
1de202e927 (refs #554)Search box bug fix 2014-11-19 07:17:37 +09:00
Naoki Takezoe
4eb9f4a485 (refs #551)Adjust wiki buttons 2014-11-17 17:08:51 +09:00
Naoki Takezoe
a8801e4e41 (refs #432)Show the information message at the top page 2014-11-17 00:52:51 +09:00
Naoki Takezoe
ee1c84dbf2 (refs #508)Remove filter parameter 2014-11-16 20:22:36 +09:00
Naoki Takezoe
e40e1fa6cd (refs #508)Add search filter box to the dashboard 2014-11-16 01:38:31 +09:00
Naoki Takezoe
055f648ea2 (refs #508)Remove repository filter 2014-11-11 13:10:07 +09:00
Naoki Takezoe
37a399c3a2 (refs #508)Fix line separator 2014-11-11 12:09:48 +09:00
Naoki Takezoe
bc0b11b60a (refs #508)Basic filter box implementation 2014-11-11 12:07:22 +09:00
Naoki Takezoe
65a1ca7146 (refs #508)Start to add search filter box 2014-11-11 03:19:51 +09:00
Naoki Takezoe
2293030d4e Merge pull request #541 from rlazoti/fix-dashboard
Fix pull request's view on dashboard
2014-11-09 00:21:39 +09:00
Rodrigo Lazoti
c83fab611e Remove the 'All' tab 2014-11-06 19:01:16 -02:00
Rodrigo Lazoti
29baf1223c Fix pull request's view on dashboard 2014-11-04 18:15:07 -02:00
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
shimamoto
ff4052f097 (refs #507) Fix the issue info of conversation page. 2014-10-19 23:39:25 +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
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
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
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
bati11
ebb9d9329a Merge branch 'master' into feature-tasklist 2014-05-03 10:51:18 +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
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
189 changed files with 5868 additions and 4181 deletions

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: scala
scala:
- 2.11.2

View File

@@ -1,4 +1,4 @@
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 [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://travis-ci.org/takezoe/gitbucket.svg?branch=master)](https://travis-ci.org/takezoe/gitbucket)
=========
GitBucket is the easily installable Github clone written with Scala.
@@ -23,7 +23,6 @@ The current version of GitBucket provides a basic features below:
Following features are not implemented, but we will make them in the future release!
- Comment for the changeset
- Network graph
- Statistics
- Watch / Star
@@ -80,6 +79,51 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 2.7 - 29 Dec 2014
- Comment for commit and diff
- Fix security issue in markdown rendering
- Some bug fix and improvements
### 2.6 - 24 Nov 2014
- Search box at issues and pull requests
- Information from administrator
- Pull request UI has been updated
- Move to TravisCI from Buildhive
- Some bug fix and improvements
### 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

View File

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

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
#
# /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
#
@@ -8,28 +10,44 @@
# description: Run GitBucket server
# processname: java
# Source function library
. /etc/rc.d/init.d/functions
set -e
[ -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
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# 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
LOG_FILE=/var/log/gitbucket/run.log
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
PID_FILE=/var/run/gitbucket.pid
# Default return value
RETVAL=0
RED='\033[1m\E[37;41m'
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() {
echo -n $"Starting GitBucket server: "
# Compile statup parameters
START_OPTS=
if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi
@@ -40,17 +58,15 @@ start() {
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
RETVAL=$?
# Store PID of the Java process into a file
echo $! > $PID_FILE
if [ $RETVAL -eq 0 ] ; then
success "GitBucket startup"
success "Success"
else
failure "GitBucket startup"
failure "Exit code $RETVAL"
fi
echo
@@ -82,25 +98,41 @@ restart() {
start
}
case "$1" in
start)
## MacOS proxies for System V service hooks:
StartService() {
start
;;
stop)
}
StopService() {
stop
;;
restart)
}
RestartService() {
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

@@ -0,0 +1,15 @@
# Contrib Notes #
RPM spec file and init script for Red Hat Enterprise Linux 6.x.
To create RPM:
1. Edit `../../gitbucket.conf` to suit.
2. Edit `gitbucket.init` to suit.
3. Edit `gitbucket.spec` to suit.
4. Place `gitbucket.spec` to rpm/SPECS/.
5. Place `gitbucket.init` and `gitbucket.war` to rpm/SOURCES/.
6. Execute `rpmbuild -ba rpm/SPECS/gitbucket.spec`
This rpm runs gitbucket not as root user but as gitbucket user.
This rpm creates user and group named `gitbucket` at installation.
This rpm make chkconfig of gitbucket to be on.

View File

@@ -0,0 +1,108 @@
#!/bin/bash
#
# RedHat: /etc/rc.d/init.d/gitbucket
#
# Starts the GitBucket server
#
# chkconfig: 345 60 40
# description: Run GitBucket server
# processname: java
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
# Default values
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# Pull in cq settings
[ -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
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
RED='\033[1m\E[37;41m'
GREEN='\033[1m\E[37;42m'
OFF='\E[0m'
RETVAL=0
start() {
echo -n $"Starting GitBucket server: "
START_OPTS=
if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi
if [ $GITBUCKET_PREFIX ]; then
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
fi
if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
GITBUCKET_HOME="${GITBUCKET_HOME}" daemon --user=gitbucket java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
sleep 3
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ] ; then
success "Success"
else
failure "Exit code $RETVAL"
fi
echo
return $RETVAL
}
stop() {
echo -n $"Stopping GitBucket server: "
# Run the Java process
pkill -f $GITBUCKET_WAR_FILE >>$LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ] ; then
success "GitBucket stopping"
else
failure "GitBucket stopping"
fi
echo
return $RETVAL
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ]; then
echo $"GitBucket is running...."
else
echo $"GitBucket is stopped"
fi
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
exit $RETVAL

View File

@@ -1,6 +1,6 @@
Name: gitbucket
Summary: GitHub clone written with Scala.
Version: 1.7
Version: 2.6
Release: 1%{?dist}
License: Apache
URL: https://github.com/takezoe/gitbucket
@@ -26,6 +26,25 @@ GitBucket is the easily installable GitHub clone written with Scala.
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
%pre
/usr/sbin/groupadd -r gitbucket &> /dev/null || :
/usr/sbin/useradd -g gitbucket -s /bin/false -r -c "GitBucket GitHub clone" -d %{_sharedstatedir}/%{name} gitbucket &> /dev/null || :
%post
/sbin/chkconfig --add gitbucket
%preun
if [ "$1" = 0 ]; then
/sbin/service gitbucket stop > /dev/null 2>&1
/sbin/chkconfig --del gitbucket
fi
exit 0
%postun
if [ "$1" -ge 1 ]; then
/sbin/service gitbucket restart > /dev/null 2>&1
fi
exit 0
%clean
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
@@ -34,12 +53,28 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
%files
%defattr(-,root,root,-)
%{_datarootdir}/%{name}/lib/%{name}.war
%{_sysconfdir}/init.d/%{name}
%config %{_sysconfdir}/sysconfig/%{name}
%{_localstatedir}/log/%{name}/run.log
%config %{_sysconfdir}/init.d/%{name}
%config(noreplace) %{_sysconfdir}/sysconfig/%{name}
%attr(0755,gitbucket,gitbucket) %{_sharedstatedir}/%{name}
%attr(0750,gitbucket,gitbucket) %{_localstatedir}/log/%{name}
%changelog
* Mon Nov 24 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.6
* Sun Nov 09 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.5
* Sun Oct 26 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.4.1
* Mon Jul 21 2014 Toru Takahashi <torutk at gmail.com>
- execute as gitbucket user
* Sun Jul 20 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.1.
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
- Version bump to v1.7.

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"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@@ -7,14 +14,15 @@
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-Dmail.smtp.starttls.enable=true</string>
<string>$GITBUCKET_JVM_OPTS</string>
<string>-jar</string>
<string>gitbucket.war</string>
<string>--host=127.0.0.1</string>
<string>--port=8080</string>
<string>--host=$GITBUCKET_HOST</string>
<string>--port=$GITBUCKET_PORT</string>
<string>--https=true</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</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: 87 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

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

View File

@@ -1,57 +1,60 @@
import sbt._
import Keys._
import org.scalatra.sbt._
import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
object MyBuild extends Build {
val Organization = "jp.sf.amateras"
val Name = "gitbucket"
val Version = "0.0.1"
val ScalaVersion = "2.10.3"
val ScalatraVersion = "2.2.1"
val ScalaVersion = "2.11.2"
val ScalatraVersion = "2.3.0"
lazy val project = Project (
"gitbucket",
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" % "2.0.2",
"org.mozilla" % "rhino" % "1.7R4",
"com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"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: _*)
file(".")
)
.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")
resolvers += "spray repo" at "http://repo.spray.io"
addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")

View File

@@ -1,2 +1,2 @@
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.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.IOException;
import java.io.File;
import java.net.URL;
import java.security.ProtectionDomain;
@@ -44,6 +42,14 @@ public class JettyLauncher {
server.addConnector(connector);
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();
URL location = domain.getCodeSource().getLocation();
@@ -59,4 +65,27 @@ public class JettyLauncher {
server.start();
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

@@ -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

@@ -0,0 +1,18 @@
CREATE TABLE COMMIT_COMMENT (
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
COMMIT_ID VARCHAR(100) NOT NULL,
COMMENT_ID INT AUTO_INCREMENT,
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
CONTENT TEXT NOT NULL,
FILE_NAME NVARCHAR(100),
OLD_LINE_NUMBER INT,
NEW_LINE_NUMBER INT,
REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL,
PULL_REQUEST BOOLEAN NOT NULL
);
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_PK PRIMARY KEY (COMMENT_ID);
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, COMMENT_ID);

View File

@@ -335,7 +335,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}

View File

@@ -24,8 +24,9 @@ abstract class ControllerBase extends ScalatraFilter
implicit val jsonFormats = DefaultFormats
// Don't set content type via Accept header.
override def format(implicit request: HttpServletRequest) = ""
// TODO Scala 2.11
// // 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 {
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,
includeContextPath: Boolean = true, includeServletPath: Boolean = true)
(implicit request: HttpServletRequest, response: HttpServletResponse) =
// TODO Scala 2.11
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
includeContextPath: Boolean = true, includeServletPath: Boolean = true,
absolutize: Boolean = true, withSessionId: Boolean = true)
(implicit request: HttpServletRequest, response: HttpServletResponse): String =
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,18 +1,33 @@
package app
import service._
import util.{UsersAuthenticator, Keys}
import util.{StringUtil, UsersAuthenticator, Keys}
import util.Implicits._
import service.IssuesService.IssueSearchCondition
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator
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")(usersOnly {
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
case _ => searchIssues("created_by")
}
} getOrElse {
searchIssues("created_by")
}
})
get("/dashboard/issues/assigned")(usersOnly {
@@ -23,86 +38,99 @@ trait DashboardControllerBase extends ControllerBase {
searchIssues("created_by")
})
get("/dashboard/issues/mentioned")(usersOnly {
searchIssues("mentioned")
})
get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None)
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
case _ => searchPullRequests("created_by")
}
} getOrElse {
searchPullRequests("created_by")
}
})
get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None)
get("/dashboard/pulls/created_by")(usersOnly {
searchPullRequests("created_by")
})
get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None)
get("/dashboard/pulls/assigned")(usersOnly {
searchPullRequests("assigned")
})
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
get("/dashboard/pulls/mentioned")(usersOnly {
searchPullRequests("mentioned")
})
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
val condition = session.putAndGet(key, if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, Map[String, Int]())
}
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
filter match {
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
}
}
private def searchIssues(filter: String) = {
import IssuesService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
)
val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request)
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
//
dashboard.html.issues(
issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page,
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
condition),
countIssue(condition, Map.empty, false, repositories: _*),
countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
condition,
filter)
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page,
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter,
getGroupNames(userName))
}
private def searchPullRequests(filter: String, repository: Option[String]) = {
private def searchPullRequests(filter: String) = {
import IssuesService._
import PullRequestService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
}.copy(repo = repository))
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
val allRepos = getAllRepositories(userName)
val page = IssueSearchCondition.page(request)
dashboard.html.pulls(
pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
page,
countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
condition,
None,
false),
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
getRepositoryNamesOfUser(userName).map { RepoName =>
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
}.sortBy(_._3).reverse,
condition,
filter)
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page,
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter,
getGroupNames(userName))
}

View File

@@ -20,11 +20,23 @@ trait IndexControllerBase extends ControllerBase {
get("/"){
val loginAccount = context.loginAccount
if(loginAccount.isEmpty) {
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
} else {
val loginUserName = loginAccount.get.userName
val loginUserGroups = getGroupsByUserName(loginUserName)
var visibleOwnerSet : Set[String] = Set(loginUserName)
visibleOwnerSet ++= loginUserGroups
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
html.index(getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
}
}
get("/signin"){
@@ -59,6 +71,10 @@ trait IndexControllerBase extends ControllerBase {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
if(LDAPUtil.isDummyMailAddress(account)) {
redirect("/" + account.userName + "/_edit")
}
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/")
@@ -72,8 +88,6 @@ trait IndexControllerBase extends ControllerBase {
/**
* JSON API for collaborator completion.
*
* TODO Move to other controller?
*/
get("/_user/proposals")(usersOnly {
contentType = formats("json")
@@ -82,5 +96,11 @@ trait IndexControllerBase extends ControllerBase {
)
})
/**
* JSON APU for checking user existence.
*/
post("/_user/existence")(usersOnly {
getAccountByUserName(params("userName")).isDefined
})
}

View File

@@ -20,7 +20,6 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
@@ -32,10 +31,12 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply)
val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required)))
)(x => x)
val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text()))
)(IssueEditForm.apply)
"content" -> trim(optional(text()))
)(x => x)
val commentForm = mapping(
"issueId" -> label("Issue Id", number()),
@@ -47,16 +48,13 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _)
})
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _)
})
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _)
get("/:owner/:repository/issues")(referrersOnly { repository =>
val q = request.getParameter("q")
if(Option(q).exists(_.contains("is:pr"))){
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
} else {
searchIssues(repository)
}
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -125,14 +123,29 @@ trait IssuesControllerBase extends ControllerBase {
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, form.title, form.content)
updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
createReferComment(owner, name, issue.copy(title = title), title)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse(""))
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
@@ -180,13 +193,13 @@ trait IssuesControllerBase extends ControllerBase {
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect {
case t if t == "html" => issues.html.editissue(
x.title, x.content, x.issueId, x.userName, x.repositoryName)
x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true)
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
}
} else Unauthorized
@@ -203,7 +216,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true)
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
}
} else Unauthorized
@@ -234,15 +247,17 @@ trait IssuesControllerBase extends ControllerBase {
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound
} getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action =>
executeBatch(repository) {
handleComment(_, None, repository)( _ => action)
action match {
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
case _ => // TODO BadRequest
}
}
})
@@ -292,7 +307,10 @@ trait IssuesControllerBase extends ControllerBase {
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues")
params("from") match {
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
}
}
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
@@ -318,15 +336,15 @@ trait IssuesControllerBase extends ControllerBase {
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") ->
Some(recordReopenIssueActivity _))
}
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" if(issue.closed) => false ->
(Some("reopen") -> Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
t
}
updateClosed(owner, name, issueId, closed)
t
}
.getOrElse(None -> None)
val commentId = content
@@ -336,7 +354,7 @@ trait IssuesControllerBase extends ControllerBase {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
}
// record activity
// record comment activity if comment is entered
content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _)
@@ -369,32 +387,33 @@ trait IssuesControllerBase extends ControllerBase {
}
}
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
}
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
"issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}

View File

@@ -2,51 +2,67 @@ package app
import jp.sf.amateras.scalatra.forms._
import service._
import util.CollaboratorsAuthenticator
import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import util.Implicits._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
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 {
self: LabelsService with RepositoryService with CollaboratorsAuthenticator =>
self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String)
val newForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color)))
val labelForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
val editForm = mapping(
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"editColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
issues.labels.html.list(
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { 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 =>
issues.labels.html.edit(Some(label), repository)
} 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))
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)
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
Ok()
})
/**

View File

@@ -1,6 +1,6 @@
package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
import util._
import util.Directory._
import util.Implicits._
import util.ControlUtil._
@@ -12,20 +12,21 @@ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
import util.JGitUtil.DiffInfo
import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitsService with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
@@ -59,11 +60,12 @@ trait PullRequestsControllerBase extends ControllerBase {
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
searchPullRequests(None, repository)
})
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
val q = request.getParameter("q")
if(Option(q).exists(_.contains("is:issue"))){
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
} else {
searchPullRequests(None, repository)
}
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
@@ -77,7 +79,8 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
.sortWith((a, b) => a.registeredDate before b.registeredDate),
getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
@@ -156,7 +159,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
mergeCommit.setAuthor(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)
// insertObject and got mergeCommit Object Id
@@ -277,6 +280,7 @@ trait PullRequestsControllerBase extends ControllerBase {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
originBranch,
forkedBranch,
oldId.getName,
@@ -443,7 +447,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit)
}.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)
@@ -453,7 +457,6 @@ trait PullRequestsControllerBase extends ControllerBase {
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
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 sessionKey = Keys.Session.Pulls(owner, repoName)
@@ -463,14 +466,15 @@ trait PullRequestsControllerBase extends ControllerBase {
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
userName,
issues.html.list(
"pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
countIssue(condition, Map.empty, true, owner -> repoName),
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -2,10 +2,8 @@ package app
import service._
import util.Directory._
import util.ControlUtil._
import util.Implicits._
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import util.JGitUtil.CommitInfo
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
@@ -13,6 +11,7 @@ import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
import util.ControlUtil._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService
@@ -71,11 +70,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
saveRepositoryOptions(
repository.owner,
repository.name,
form.description,
if(repository.branchList.isEmpty) "master" else form.defaultBranch,
defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate
@@ -93,6 +93,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
}
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
})
@@ -131,7 +135,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
})
/**
@@ -153,7 +157,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/**
* Send the test request to registered web hook URLs.
*/
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = git.log
@@ -161,15 +165,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
}
case _ =>
getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook(repository.owner, repository.name,
List(model.WebHook(repository.owner, repository.name, form.url)),
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
)
}
flash += "url" -> form.url
flash += "info" -> "Test payload deployed!"
}
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")

View File

@@ -8,23 +8,31 @@ import _root_.util._
import service._
import org.scalatra._
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.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream}
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
import org.eclipse.jgit.revwalk.RevCommit
import service.WebHookService.WebHookPayload
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 CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
/**
* The repository viewer.
*/
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 CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class EditorForm(
branch: String,
@@ -32,6 +40,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
content: String,
message: Option[String],
charset: String,
lineSeparator: String,
newFileName: String,
oldFileName: Option[String]
)
@@ -43,14 +52,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileName: String
)
case class CommentForm(
fileName: Option[String],
oldLineNumber: Option[Int],
newLineNumber: Option[Int],
content: String,
pullRequest: Boolean
)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))),
"lineSeparator" -> trim(label("Line Separator", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply)
val deleteForm = mapping(
@@ -60,6 +78,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"fileName" -> trim(label("Filename", text(required)))
)(DeleteForm.apply)
val commentForm = mapping(
"fileName" -> trim(label("Filename", optional(text()))),
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
"newLineNumber" -> trim(label("New line number", optional(number()))),
"content" -> trim(label("Content", text(required))),
"pullRequest" -> trim(label("In pull request", boolean()))
)(CommentForm.apply)
/**
* Returns converted HTML from Markdown for preview.
*/
@@ -67,7 +93,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean)
params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
@@ -101,8 +129,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound
}
}
@@ -142,7 +170,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
})
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}"))
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
@@ -151,7 +180,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
})
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)){
form.message.getOrElse(s"Update ${form.newFileName}")
} else {
@@ -179,6 +209,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
@@ -188,7 +219,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
} else {
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
}
@@ -206,12 +237,82 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
repository, diffs, oldCommitId)
getCommitComments(repository.owner, repository.name, id, false),
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
}
}
})
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
})
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
val id = params("id")
val fileName = params.get("fileName")
val oldLineNumber = params.get("oldLineNumber") flatMap {b => Some(b.toInt)}
val newLineNumber = params.get("newLineNumber") flatMap {b => Some(b.toInt)}
val pullRequest = params.get("pullRequest")
repo.html.commentform(
commitId = id,
fileName, oldLineNumber, newLineNumber, pullRequest.map(_.toBoolean).getOrElse(false),
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
repository = repository
)
})
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.pullRequest)
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect {
case t if t == "html" => repo.html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
}
} else Unauthorized
} getOrElse NotFound
})
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateCommitComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteCommitComment(comment.commentId))
} else Unauthorized
} getOrElse NotFound
}
})
/**
* Displays branches.
*/
@@ -226,6 +327,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.
*/
@@ -252,50 +371,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Download repository contents as an archive.
*/
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
val name = multiParams("splat").head
if(name.endsWith(".zip")){
val revision = name.stripSuffix(".zip")
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists){
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
multiParams("splat").head match {
case name if name.endsWith(".zip") =>
archiveRepository(name, ".zip", repository)
case name if name.endsWith(".tar.gz") =>
archiveRepository(name, ".tar.gz", repository)
case _ => BadRequest
}
})
@@ -337,10 +418,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} else {
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
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
// get files
val files = JGitUtil.getFileList(git, revision, path)
val parentPath = if (path == ".") Nil else path.split("/").toList
@@ -355,8 +436,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
flash.get("info"), flash.get("error"))
}
} getOrElse NotFound
}
@@ -376,7 +458,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(s"refs/heads/${branch}")
val headTip = git.getRepository.resolve(headName)
JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
@@ -391,7 +473,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
builder.finish()
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.release()
@@ -408,8 +490,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
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 +522,32 @@ 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
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
}

View File

@@ -10,6 +10,8 @@ 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
with AccountService with AdminAuthenticator
@@ -19,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
@@ -41,8 +44,9 @@ trait SystemSettingsControllerBase extends ControllerBase {
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", 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()))),
"mailAttribute" -> trim(label("Mail address attribute", text(required))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
@@ -81,44 +85,57 @@ trait SystemSettingsControllerBase extends ControllerBase {
redirect("/admin/system")
})
// TODO Enable commented code to enable plug-in system
// get("/admin/plugins")(adminOnly {
// val installedPlugins = plugin.PluginSystem.plugins
// val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
// admin.plugins.html.installed(installedPlugins, updatablePlugins)
// })
//
// post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
// deletePlugins(form.pluginIds)
// installPlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
//
// post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
// deletePlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
//
// get("/admin/plugins/available")(adminOnly {
// val installedPlugins = plugin.PluginSystem.plugins
// val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
// admin.plugins.html.available(availablePlugins)
// })
//
// post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
// installPlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
get("/admin/plugins")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
} else NotFound
})
// get("/admin/plugins/console")(adminOnly {
// admin.plugins.html.console()
// })
//
// post("/admin/plugins/console")(adminOnly {
// val script = request.getParameter("script")
// val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
// Ok(result)
// })
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 = {
@@ -137,9 +154,10 @@ trait SystemSettingsControllerBase extends ControllerBase {
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(!pluginDir.exists){
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
if(pluginDir.exists){
FileUtils.deleteDirectory(pluginDir)
}
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
PluginSystem.installPlugin(plugin.id)
}
}

View File

@@ -49,7 +49,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply)
val newGroupForm = mapping(
@@ -182,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(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
@@ -195,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

@@ -21,20 +21,19 @@ trait AccountComponent { self: Profile =>
val removed = column[Boolean]("REMOVED")
def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply)
}
case class Account(
userName: String,
fullName: String,
mailAddress: String,
password: String,
isAdmin: Boolean,
url: Option[String],
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date],
image: Option[String],
isGroupAccount: Boolean,
isRemoved: Boolean
)
}
case class Account(
userName: String,
fullName: String,
mailAddress: String,
password: String,
isAdmin: Boolean,
url: Option[String],
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date],
image: Option[String],
isGroupAccount: Boolean,
isRemoved: Boolean
)

View File

@@ -15,15 +15,15 @@ trait ActivityComponent extends TemplateComponent { self: Profile =>
val activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
}
case class Activity(
userName: String,
repositoryName: String,
activityUserName: String,
activityType: String,
message: String,
additionalInfo: Option[String],
activityDate: java.util.Date,
activityId: Int = 0
)
}
case class Activity(
userName: String,
repositoryName: String,
activityUserName: String,
activityType: String,
message: String,
additionalInfo: Option[String],
activityDate: java.util.Date,
activityId: Int = 0
)

View File

@@ -8,40 +8,47 @@ protected[model] trait TemplateComponent { self: Profile =>
val repositoryName = column[String]("REPOSITORY_NAME")
def byRepository(owner: String, repository: String) =
(userName is owner.bind) && (repositoryName is repository.bind)
(userName === owner.bind) && (repositoryName === repository.bind)
def byRepository(userName: Column[String], repositoryName: Column[String]) =
(this.userName is userName) && (this.repositoryName is repositoryName)
(this.userName === userName) && (this.repositoryName === repositoryName)
}
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)
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)
byRepository(userName, repositoryName) && (this.issueId === issueId)
}
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)
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)
byRepository(userName, repositoryName) && (this.labelId === labelId)
}
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)
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)
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
}
trait CommitTemplate extends BasicTemplate { self: Table[_] =>
val commitId = column[String]("COMMIT_ID")
def byCommit(owner: String, repository: String, commitId: String) =
byRepository(owner, repository) && (this.commitId === commitId)
}
}

View File

@@ -10,12 +10,12 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply)
def byPrimaryKey(owner: String, repository: String, collaborator: String) =
byRepository(owner, repository) && (collaboratorName is collaborator.bind)
byRepository(owner, repository) && (collaboratorName === collaborator.bind)
}
case class Collaborator(
userName: String,
repositoryName: String,
collaboratorName: String
)
}
case class Collaborator(
userName: String,
repositoryName: String,
collaboratorName: String
)

View File

@@ -0,0 +1,78 @@
package model
trait Comment {
val commentedUserName: String
val registeredDate: java.util.Date
}
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
def autoInc = this returning this.map(_.commentId)
}
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
val commentId = column[Int]("COMMENT_ID", O AutoInc)
val action = column[String]("ACTION")
val commentedUserName = column[String]("COMMENTED_USER_NAME")
val content = column[String]("CONTENT")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}
}
case class IssueComment (
userName: String,
repositoryName: String,
issueId: Int,
commentId: Int = 0,
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
) extends Comment
trait CommitCommentComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val CommitComments = new TableQuery(tag => new CommitComments(tag)){
def autoInc = this returning this.map(_.commentId)
}
class CommitComments(tag: Tag) extends Table[CommitComment](tag, "COMMIT_COMMENT") with CommitTemplate {
val commentId = column[Int]("COMMENT_ID", O AutoInc)
val commentedUserName = column[String]("COMMENTED_USER_NAME")
val content = column[String]("CONTENT")
val fileName = column[Option[String]]("FILE_NAME")
val oldLine = column[Option[Int]]("OLD_LINE_NUMBER")
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val pullRequest = column[Boolean]("PULL_REQUEST")
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}
}
case class CommitComment(
userName: String,
repositoryName: String,
commitId: String,
commentId: Int = 0,
commentedUserName: String,
content: String,
fileName: Option[String],
oldLine: Option[Int],
newLine: Option[Int],
registeredDate: java.util.Date,
updatedDate: java.util.Date,
pullRequest: Boolean
) extends Comment

View File

@@ -11,10 +11,10 @@ trait GroupMemberComponent { self: Profile =>
val isManager = column[Boolean]("MANAGER")
def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply)
}
case class GroupMember(
groupName: String,
userName: String,
isManager: Boolean
)
}
case class GroupMember(
groupName: String,
userName: String,
isManager: Boolean
)

View File

@@ -31,18 +31,19 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
}
case class Issue(
userName: String,
repositoryName: String,
issueId: Int,
openedUserName: String,
milestoneId: Option[Int],
assignedUserName: Option[String],
title: String,
content: Option[String],
closed: Boolean,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
isPullRequest: Boolean)
}
case class Issue(
userName: String,
repositoryName: String,
issueId: Int,
openedUserName: String,
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,34 +0,0 @@
package model
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
def autoInc = this returning this.map(_.commentId)
}
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
val commentId = column[Int]("COMMENT_ID", O AutoInc)
val action = column[String]("ACTION")
val commentedUserName = column[String]("COMMENTED_USER_NAME")
val content = column[String]("CONTENT")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
}
case class IssueComment(
userName: String,
repositoryName: String,
issueId: Int,
commentId: Int = 0,
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
)
}

View File

@@ -8,12 +8,13 @@ trait IssueLabelComponent extends TemplateComponent { self: Profile =>
class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate {
def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
byIssue(owner, repository, issueId) && (this.labelId is labelId.bind)
byIssue(owner, repository, issueId) && (this.labelId === labelId.bind)
}
case class IssueLabel(
userName: String,
repositoryName: String,
issueId: Int,
labelId: Int)
}
case class IssueLabel(
userName: String,
repositoryName: String,
issueId: Int,
labelId: Int
)

View File

@@ -14,24 +14,24 @@ trait LabelComponent extends TemplateComponent { self: Profile =>
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(
userName: String,
repositoryName: String,
labelId: Int = 0,
labelName: String,
color: String){
case class Label(
userName: String,
repositoryName: String,
labelId: Int = 0,
labelName: String,
color: String){
val fontColor = {
val r = color.substring(0, 2)
val g = color.substring(2, 4)
val b = color.substring(4, 6)
val fontColor = {
val r = color.substring(0, 2)
val g = color.substring(2, 4)
val b = color.substring(4, 6)
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000"
} else {
"FFFFFF"
}
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000"
} else {
"ffffff"
}
}
}

View File

@@ -17,13 +17,14 @@ trait MilestoneComponent extends TemplateComponent { self: Profile =>
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
}
case class Milestone(
userName: String,
repositoryName: String,
milestoneId: Int = 0,
title: String,
description: Option[String],
dueDate: Option[java.util.Date],
closedDate: Option[java.util.Date])
}
case class Milestone(
userName: String,
repositoryName: String,
milestoneId: Int = 0,
title: String,
description: Option[String],
dueDate: 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

@@ -1,9 +1,7 @@
package model
import slick.driver.JdbcProfile
trait Profile {
val profile: JdbcProfile
val profile: slick.driver.JdbcProfile
import profile.simple._
// java.util.Date Mapped Column Types
@@ -17,3 +15,29 @@ trait Profile {
}
}
object Profile extends {
val profile = slick.driver.H2Driver
} with AccountComponent
with ActivityComponent
with CollaboratorComponent
with CommitCommentComponent
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

@@ -17,16 +17,16 @@ trait PullRequestComponent extends TemplateComponent { self: Profile =>
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
}
case class PullRequest(
userName: String,
repositoryName: String,
issueId: Int,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String
)
}
case class PullRequest(
userName: String,
repositoryName: String,
issueId: Int,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String
)

View File

@@ -21,19 +21,19 @@ trait RepositoryComponent extends TemplateComponent { self: Profile =>
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
case class Repository(
userName: String,
repositoryName: String,
isPrivate: Boolean,
description: Option[String],
defaultBranch: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastActivityDate: java.util.Date,
originUserName: Option[String],
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String]
)
}
case class Repository(
userName: String,
repositoryName: String,
isPrivate: Boolean,
description: Option[String],
defaultBranch: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastActivityDate: java.util.Date,
originUserName: Option[String],
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String]
)

View File

@@ -12,13 +12,13 @@ trait SshKeyComponent { self: Profile =>
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(
userName: String,
sshKeyId: Int = 0,
title: String,
publicKey: String
)
}
case class SshKey(
userName: String,
sshKeyId: Int = 0,
title: String,
publicKey: String
)

View File

@@ -9,12 +9,12 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
val url = column[String]("URL")
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
}
case class WebHook(
userName: String,
repositoryName: String,
url: String
)
}
case class WebHook(
userName: String,
repositoryName: String,
url: String
)

View File

@@ -1,24 +1,3 @@
package object model extends {
// TODO [Slick 2.0]Should be configurable?
val profile = slick.driver.H2Driver
// TODO [Slick 2.0]To avoid compilation error about delete invocation. Why can't this error be resolved by import profile.simple._?
val simple = profile.simple
} 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 Profile {
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
package object model {
type Session = slick.jdbc.JdbcBackend#Session
}

View File

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

View File

@@ -1,6 +1,7 @@
package plugin
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
import plugin.PluginSystem._
import java.sql.Connection
trait Plugin {
val id: String
@@ -9,8 +10,13 @@ trait Plugin {
val url: String
val description: String
def repositoryMenus : List[RepositoryMenu]
def globalMenus : List[GlobalMenu]
def repositoryActions : List[Action]
def globalActions : List[Action]
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

@@ -1,19 +1,24 @@
package plugin
import app.Context
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
import util.Directory._
import util.ControlUtil._
import org.apache.commons.io.FileUtils
import util.JGitUtil
import org.eclipse.jgit.api.Git
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 {
object PluginSystem extends PluginService {
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
@@ -27,8 +32,21 @@ object PluginSystem {
def plugins: List[Plugin] = pluginsMap.values.toList
def uninstall(id: String): Unit = {
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
@@ -36,7 +54,7 @@ object PluginSystem {
/**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/
def init(): Unit = {
def init()(implicit session: Session): Unit = {
if(initialized.compareAndSet(false, true)){
// Load installed plugins
val pluginDir = new java.io.File(PluginHome)
@@ -51,41 +69,107 @@ object PluginSystem {
}
// TODO Method name seems to not so good.
def installPlugin(id: String): Unit = {
val pluginDir = new java.io.File(PluginHome)
val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js")
def installPlugin(id: String)(implicit session: Session): Unit = {
val pluginHome = new java.io.File(PluginHome)
val pluginDir = new java.io.File(pluginHome, id)
if(javaScriptFile.exists && javaScriptFile.isFile){
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, id + "/plugin.properties"))){ in =>
using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
properties.load(in)
}
val script = FileUtils.readFileToString(javaScriptFile, "UTF-8")
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 {
JavaScriptPlugin.evaluateJavaScript(script, Map(
"id" -> properties.getProperty("id"),
"version" -> properties.getProperty("version"),
"author" -> properties.getProperty("author"),
"url" -> properties.getProperty("url"),
"description" -> properties.getProperty("description")
))
// Compile and eval Scala source code
ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
ScalaPlugin.compileTemplate(
id.replace("-", ""),
file.getName.stripSuffix(".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: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e)
case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
}
}
}
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[Action] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
// 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(path: String, function: (HttpServletRequest, HttpServletResponse) => Any)
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.
@@ -107,17 +191,4 @@ object PluginSystem {
}
}
// TODO This is a test
// addGlobalMenu("Google", "http://www.google.co.jp/", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => context.loginAccount.isDefined }
//
// addRepositoryMenu("Board", "board", "/board", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => true}
//
// addGlobalAction("/hello"){ (request, response) =>
// "Hello World!"
// }
}

View File

@@ -50,18 +50,17 @@ class PluginUpdateJob extends Job {
object PluginUpdateJob {
def schedule(scheduler: Scheduler): Unit = {
// TODO Enable commented code to enable plug-in system
// val job = newJob(classOf[PluginUpdateJob])
// .withIdentity("pluginUpdateJob")
// .build()
//
// val trigger = newTrigger()
// .withIdentity("pluginUpdateTrigger")
// .startNow()
// .withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
// .build()
//
// scheduler.scheduleJob(job, trigger)
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

@@ -1,9 +1,16 @@
package plugin
import app.Context
import scala.collection.mutable.ListBuffer
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
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,
@@ -11,13 +18,15 @@ class ScalaPlugin(val id: String, val version: String,
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[Action]()
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[Action] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
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)
@@ -27,12 +36,42 @@ class ScalaPlugin(val id: String, val version: String,
globalMenuList += GlobalMenu(label, url, icon, condition)
}
def addGlobalAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
globalActionList += Action(path, function)
def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
globalActionList += Action(method, path, security, function)
}
def addRepositoryAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
repositoryActionList += Action(path, 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,9 +1,10 @@
package service
import model._
import simple._
import model.Profile._
import profile.simple._
import model.{Account, GroupMember}
// TODO [Slick 2.0]NOT import directly?
import model.dateColumnType
import model.Profile.dateColumnType
import service.SystemSettingsService.SystemSettings
import util.StringUtil._
import util.LDAPUtil
@@ -39,7 +40,11 @@ trait AccountService {
// Create or update account by LDAP information
getAccountByUserName(ldapUserInfo.userName, true) match {
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)
}
case Some(x) if(x.isRemoved) => {
@@ -70,16 +75,16 @@ trait AccountService {
}
def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
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)(implicit s: Session): Option[Account] =
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)(implicit s: Session): List[Account] =
if(includeRemoved){
Accounts sortBy(_.userName) list
} else {
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])
@@ -100,7 +105,7 @@ trait AccountService {
def updateAccount(account: Account)(implicit s: Session): Unit =
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) }
.update (
account.password,
@@ -114,10 +119,10 @@ trait AccountService {
account.isRemoved)
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)(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])(implicit s: Session): Unit =
Accounts insert Account(
@@ -135,10 +140,10 @@ trait AccountService {
isRemoved = false)
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)])(implicit s: Session): Unit = {
GroupMembers.filter(_.groupName is groupName.bind).delete
GroupMembers.filter(_.groupName === groupName.bind).delete
members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager)
}
@@ -146,21 +151,26 @@ trait AccountService {
def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] =
GroupMembers
.filter(_.groupName is groupName.bind)
.filter(_.groupName === groupName.bind)
.sortBy(_.userName)
.list
def getGroupsByUserName(userName: String)(implicit s: Session): List[String] =
GroupMembers
.filter(_.userName is userName.bind)
.filter(_.userName === userName.bind)
.sortBy(_.groupName)
.map(_.groupName)
.list
def removeUserRelatedData(userName: String)(implicit s: Session): Unit = {
GroupMembers.filter(_.userName is userName.bind).delete
Collaborators.filter(_.collaboratorName is userName.bind).delete
Repositories.filter(_.userName is userName.bind).delete
GroupMembers.filter(_.userName === userName.bind).delete
Collaborators.filter(_.collaboratorName === 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
}
}

View File

@@ -1,7 +1,8 @@
package service
import model._
import simple._
import model.Profile._
import profile.simple._
import model.Activity
trait ActivityService {
@@ -10,9 +11,9 @@ trait ActivityService {
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) =>
if(isPublic){
(t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind)
(t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind)
} else {
(t1.activityUserName is activityUserName.bind)
(t1.activityUserName === activityUserName.bind)
}
}
.sortBy { case (t1, t2) => t1.activityId desc }
@@ -23,7 +24,16 @@ trait ActivityService {
def getRecentActivities()(implicit s: Session): List[Activity] =
Activities
.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 }
.map { case (t1, t2) => t1 }
.take(30)
.list
def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] =
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)
@@ -85,6 +95,15 @@ trait ActivityService {
Some(cut(comment, 200)),
currentDate)
def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String)
(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_commit",
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
Some(cut(comment, 200)),
currentDate
)
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,

View File

@@ -0,0 +1,52 @@
package service
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model.Profile._
import profile.simple._
import model.CommitComment
import util.Implicits._
import util.StringUtil._
trait CommitsService {
def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) =
CommitComments filter {
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
} list
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
if (commentId forall (_.isDigit))
CommitComments filter { t =>
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
} firstOption
else
None
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int =
CommitComments.autoInc insert CommitComment(
userName = owner,
repositoryName = repository,
commitId = commitId,
commentedUserName = loginUser,
content = content,
fileName = fileName,
oldLine = oldLine,
newLine = newLine,
registeredDate = currentDate,
updatedDate = currentDate,
pullRequest = pullRequest)
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
CommitComments
.filter (_.byPrimaryKey(commentId))
.map { t =>
t.content -> t.updatedDate
}.update (content, currentDate)
def deleteCommitComment(commentId: Int)(implicit s: Session) =
CommitComments filter (_.byPrimaryKey(commentId)) delete
}

View File

@@ -3,8 +3,9 @@ package service
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model._
import simple._
import model.Profile._
import profile.simple._
import model.{Issue, IssueComment, IssueLabel, Label}
import util.Implicits._
import util.StringUtil._
@@ -42,15 +43,13 @@ trait IssuesService {
* Returns the count of the search result against issues.
*
* @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 repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): Int =
// TODO check SQL
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
/**
* Returns the Map which contains issue count for each labels.
@@ -58,13 +57,12 @@ trait IssuesService {
* @param owner the repository owner
* @param repository the repository name
* @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)
*/
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -79,47 +77,22 @@ trait IssuesService {
}
.toMap
}
/**
* Returns list which contains issue count for each repository.
* If the issue does not exist, its repository is not included in the result.
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param 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
* @return list which contains issue count for each repository
*/
def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
.groupBy { t =>
t.userName -> t.repositoryName
}
.map { case (repo, t) =>
(repo._1, repo._2, t.length)
}
.sortBy(_._3 desc)
.list
}
/**
* Returns the search result against issues.
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param pullRequest if true then returns only pull requests, false then returns only issues.
* @param offset the offset for pagination
* @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count)
*/
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, List[Label], Int)] = {
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) =>
(condition.sort match {
@@ -136,21 +109,23 @@ trait IssuesService {
.drop(offset).take(limit)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.map { case (((t1, t2), t3), t4) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
c1._1.issueId == c2._1.issueId
}
.map { issues => issues.head match {
case (issue, commentCount, _,_,_) =>
(issue,
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount)
}} toList
}
@@ -158,21 +133,18 @@ trait IssuesService {
/**
* Assembles query for conditional issue searching.
*/
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 =>
condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(t1.pullRequest is true.bind, onlyPullRequest) &&
repos
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) &&
// Label filter
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -180,7 +152,19 @@ trait IssuesService {
(t3.byRepository(t1.userName, t1.repositoryName)) &&
(t3.labelName inSetBind condition.labels)
} map(_.labelId)))
} exists, condition.labels.nonEmpty)
} exists, condition.labels.nonEmpty) &&
// Visibility filter
(Repositories filter { t2 =>
(t2.byRepository(t1.userName, t1.repositoryName)) &&
(t2.isPrivate === (condition.visibility == Some("private")).bind)
} exists, condition.visibility.nonEmpty) &&
// Organization (group) filter
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
// Mentioned filter
((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
(IssueComments filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
} exists), condition.mentioned.isDefined)
}
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
@@ -279,6 +263,7 @@ trait IssuesService {
// Search Issue
val issues = Issues
.filter(_.byRepository(owner, repository))
.innerJoin(IssueOutline).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -294,6 +279,7 @@ trait IssuesService {
// Search IssueComment
val comments = IssueComments
.filter(_.byRepository(owner, repository))
.innerJoin(Issues).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -337,22 +323,64 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None,
repo: Option[String] = None,
author: Option[String] = None,
assigned: Option[String] = None,
mentioned: Option[String] = None,
state: String = "open",
sort: String = "created",
direction: String = "desc"){
direction: String = "desc",
visibility: Option[String] = None,
groups: Set[String] = Set.empty){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
}
def nonEmpty: Boolean = !isEmpty
def toFilterString: String = (
List(
Some(s"is:${state}"),
author.map(author => s"author:${author}"),
assigned.map(assignee => s"assignee:${assignee}"),
mentioned.map(mentioned => s"mentions:${mentioned}")
).flatten ++
labels.map(label => s"label:${label}") ++
List(
milestoneId.map { _ match {
case Some(x) => s"milestone:${milestoneId}"
case None => "no:milestone"
}},
(sort, direction) match {
case ("created" , "desc") => None
case ("created" , "asc" ) => Some("sort:created-asc")
case ("comments", "desc") => Some("sort:comments-desc")
case ("comments", "asc" ) => Some("sort:comments-asc")
case ("updated" , "desc") => Some("sort:updated-desc")
case ("updated" , "asc" ) => Some("sort:updated-asc")
},
visibility.map(visibility => s"visibility:${visibility}")
).flatten ++
groups.map(group => s"group:${group}")
).mkString(" ")
def toURL: String =
"?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { id => "milestone=" + (id match {
case Some(x) => x.toString
case None => "none"
})},
repo.map("for=" + urlEncode(_)),
milestoneId.map { _ match {
case Some(x) => "milestone=" + x
case None => "milestone=none"
}},
author .map(x => "author=" + urlEncode(x)),
assigned .map(x => "assigned=" + urlEncode(x)),
mentioned.map(x => "mentioned=" + urlEncode(x)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
Some("direction=" + urlEncode(direction)),
visibility.map(x => "visibility=" + urlEncode(x)),
if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(",")))
).flatten.mkString("&")
}
@@ -363,17 +391,63 @@ object IssuesService {
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
}
/**
* Restores IssueSearchCondition instance from filter query.
*/
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
val conditions = filter.split("[  \t]+").map { x =>
val dim = x.split(":")
dim(0) -> dim(1)
}.groupBy(_._1).map { case (key, values) =>
key -> values.map(_._2).toSeq
}
val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
case "created-asc" => ("created" , "asc" )
case "comments-desc" => ("comments", "desc")
case "comments-asc" => ("comments", "asc" )
case "updated-desc" => ("comments", "desc")
case "updated-asc" => ("comments", "asc" )
case _ => ("created" , "desc")
}
IssueSearchCondition(
conditions.get("label").map(_.toSet).getOrElse(Set.empty),
conditions.get("milestone").flatMap(_.headOption) match {
case None => None
case Some("none") => Some(None)
case Some(x) => milestones.get(x).map(x => Some(x))
},
conditions.get("author").flatMap(_.headOption),
conditions.get("assignee").flatMap(_.headOption),
conditions.get("mentions").flatMap(_.headOption),
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
sort,
direction,
conditions.get("visibility").flatMap(_.headOption),
conditions.get("group").map(_.toSet).getOrElse(Set.empty)
)
}
/**
* Restores IssueSearchCondition instance from request parameters.
*/
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map{
param(request, "milestone").map {
case "none" => None
case x => x.toIntOpt
},
param(request, "for"),
param(request, "author"),
param(request, "assigned"),
param(request, "mentioned"),
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, "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
@@ -383,4 +457,6 @@ object IssuesService {
}
}
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
}

View File

@@ -1,7 +1,8 @@
package service
import model._
import simple._
import model.Profile._
import profile.simple._
import model.Label
trait LabelsService {
@@ -11,8 +12,8 @@ trait LabelsService {
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit =
Labels insert Label(
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
Labels returning Labels.map(_.labelId) += Label(
userName = owner,
repositoryName = repository,
labelName = labelName,

View File

@@ -1,9 +1,10 @@
package service
import model._
import simple._
import model.Profile._
import profile.simple._
import model.Milestone
// TODO [Slick 2.0]NOT import directly?
import model.dateColumnType
import model.Profile.dateColumnType
trait MilestonesService {
@@ -40,7 +41,7 @@ trait MilestonesService {
def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
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 }
.map { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
.toMap

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,7 +1,8 @@
package service
import model._
import simple._
import model.Profile._
import profile.simple._
import model.{PullRequest, Issue}
trait PullRequestService { self: IssuesService =>
import PullRequestService._
@@ -25,9 +26,9 @@ trait PullRequestService { self: IssuesService =>
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t2.closed is closed.bind) &&
(t1.userName is owner.get.bind, owner.isDefined) &&
(t1.repositoryName is repository.get.bind, repository.isDefined)
(t2.closed === closed.bind) &&
(t1.userName === owner.get.bind, owner.isDefined) &&
(t1.repositoryName === repository.get.bind, repository.isDefined)
}
.groupBy { case (t1, t2) => t2.openedUserName }
.map { case (userName, t) => userName -> t.length }
@@ -35,6 +36,24 @@ trait PullRequestService { self: IssuesService =>
.list
.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,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit =
@@ -54,10 +73,10 @@ trait PullRequestService { self: IssuesService =>
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t1.requestUserName is userName.bind) &&
(t1.requestRepositoryName is repositoryName.bind) &&
(t1.requestBranch is branch.bind) &&
(t2.closed is closed.bind)
(t1.requestUserName === userName.bind) &&
(t1.requestRepositoryName === repositoryName.bind) &&
(t1.requestBranch === branch.bind) &&
(t2.closed === closed.bind)
}
.map { case (t1, t2) => t1 }
.list

View File

@@ -7,8 +7,8 @@ import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git
import model._
import simple._
import model.Profile._
import profile.simple._
trait RepositorySearchService { self: IssuesService =>
import RepositorySearchService._
@@ -20,6 +20,7 @@ trait RepositorySearchService { self: IssuesService =>
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult(
issue.issueId,
issue.isPullRequest,
issue.title,
issue.openedUserName,
issue.registeredDate,
@@ -107,10 +108,11 @@ object RepositorySearchService {
case class SearchResult(
files : List[(String, String)],
issues: List[(Issue, Int, String)])
issues: List[(model.Issue, Int, String)])
case class IssueSearchResult(
issueId: Int,
isPullRequest: Boolean,
title: String,
openedUserName: String,
registeredDate: java.util.Date,

View File

@@ -1,7 +1,8 @@
package service
import model._
import simple._
import model.Profile._
import profile.simple._
import model.{Repository, Account, Collaborator}
import util.JGitUtil
trait RepositoryService { self: AccountService =>
@@ -45,40 +46,57 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
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)
PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind
t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
// Updates activity fk before deleting repository because activity is sorted by activityId
// and it can't be changed by deleting-and-inserting record.
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
Activities.filter(_.activityId === activity.activityId.bind)
.map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName)
}
deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
Issues.insertAll(issues.map { x => x.copy(
userName = newUserName,
repositoryName = newRepositoryName,
milestoneId = x.milestoneId.map { id =>
newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId
}
)} :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
@@ -86,19 +104,19 @@ trait RepositoryService { self: AccountService =>
}
// Update activity messages
val updateActivities = Activities.filter { t =>
Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId is activityId.bind).map(_.message).update(
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}@%")
}.map { t => t.activityId -> t.message }.list.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}#")
.replace(s"[commit:${oldUserName}/${oldRepositoryName}@" ,s"[commit:${newUserName}/${newRepositoryName}@")
)
}
}
@@ -108,6 +126,7 @@ trait RepositoryService { self: AccountService =>
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
@@ -117,6 +136,30 @@ trait RepositoryService { self: AccountService =>
Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
Repositories
.filter { x => (x.originUserName === userName.bind) && (x.originRepositoryName === repositoryName.bind) }
.map { x => (x.userName, x.repositoryName) }
.list
.foreach { case (userName, repositoryName) =>
Repositories
.filter(_.byRepository(userName, repositoryName))
.map(x => (x.originUserName?, x.originRepositoryName?))
.update(None, None)
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME
Repositories
.filter { x => (x.parentUserName === userName.bind) && (x.parentRepositoryName === repositoryName.bind) }
.map { x => (x.userName, x.repositoryName) }
.list
.foreach { case (userName, repositoryName) =>
Repositories
.filter(_.byRepository(userName, repositoryName))
.map(x => (x.parentUserName?, x.parentRepositoryName?))
.update(None, None)
}
}
/**
@@ -126,7 +169,7 @@ trait RepositoryService { self: AccountService =>
* @return the list of repository names
*/
def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] =
Repositories filter(_.userName is userName.bind) map (_.repositoryName) list
Repositories filter(_.userName === userName.bind) map (_.repositoryName) list
/**
* Returns the specified repository information.
@@ -140,14 +183,14 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count
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
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
issues.size,
issues.filter(_ == true).size,
issues.count(_ == true),
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
@@ -156,11 +199,28 @@ trait RepositoryService { self: AccountService =>
}
}
/**
* Returns the repositories without private repository that user does not have access right.
* Include public repository, private own repository and private but collaborator repository.
*
* @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 is userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
if(withoutPhysicalInfo){
@@ -196,13 +256,13 @@ trait RepositoryService { self: AccountService =>
case Some(x) if(x.isAdmin) => Repositories
// for Normal Users
case Some(x) if(!x.isAdmin) =>
Repositories filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) ||
(Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) ||
(Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists)
}
// for Guests
case None => Repositories filter(_.isPrivate is false.bind)
case None => Repositories filter(_.isPrivate === false.bind)
}).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse LiteralColumn(true)
repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
if(withoutPhysicalInfo){
@@ -290,15 +350,14 @@ trait RepositoryService { self: AccountService =>
}
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
// TODO check SQL
Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
}.length).first
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
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
@@ -329,4 +388,4 @@ object RepositoryService {
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
}
}

View File

@@ -1,7 +1,6 @@
package service
import model._
import slick.jdbc.JdbcBackend
import model.{Account, Issue, Session}
import util.Implicits.request2Session
/**
@@ -12,7 +11,7 @@ import util.Implicits.request2Session
*/
trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
private implicit def context2Session(implicit context: app.Context): JdbcBackend#Session =
private implicit def context2Session(implicit context: app.Context): Session =
request2Session(context.request)
def getIssue(userName: String, repositoryName: String, issueId: String)

View File

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

View File

@@ -12,6 +12,7 @@ trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
@@ -37,8 +38,9 @@ trait SystemSettingsService {
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
props.setProperty(LdapBaseDN, ldap.baseDN)
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x))
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
@@ -59,6 +61,7 @@ trait SystemSettingsService {
}
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
@@ -85,8 +88,9 @@ trait SystemSettingsService {
getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""),
getOptionValue(props, LdapAdditionalFilterCondition, None),
getOptionValue(props, LdapFullNameAttribute, None),
getValue(props, LdapMailAddressAttribute, ""),
getOptionValue(props, LdapMailAddressAttribute, None),
getOptionValue[Boolean](props, LdapTls, None),
getOptionValue(props, LdapKeystore, None)))
} else {
@@ -103,6 +107,7 @@ object SystemSettingsService {
case class SystemSettings(
baseUrl: Option[String],
information: Option[String],
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
@@ -125,8 +130,9 @@ object SystemSettingsService {
bindPassword: Option[String],
baseDN: String,
userNameAttribute: String,
additionalFilterCondition: Option[String],
fullNameAttribute: Option[String],
mailAttribute: String,
mailAttribute: Option[String],
tls: Option[Boolean],
keystore: Option[String])
@@ -144,6 +150,7 @@ object SystemSettingsService {
val DefaultLdapPort = 389
private val BaseURL = "base_url"
private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar"
private val Notification = "notification"
@@ -163,6 +170,7 @@ object SystemSettingsService {
private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute"
private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls"
@@ -187,4 +195,7 @@ object SystemSettingsService {
else value
}
// TODO temporary flag
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
}

View File

@@ -1,7 +1,8 @@
package service
import model._
import simple._
import model.Profile._
import profile.simple._
import model.{WebHook, Account}
import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo
import util.JGitUtil
@@ -43,7 +44,7 @@ trait WebHookService {
val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl =>
val f = future {
val f = Future {
logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url)
@@ -89,15 +90,15 @@ object WebHookService {
WebHookCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.time.toString,
timestamp = commit.commitTime.toString,
url = commitUrl,
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 },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser(
name = commit.committer,
email = commit.mailAddress
name = commit.committerName,
email = commit.committerEmailAddress
)
)
},

View File

@@ -64,7 +64,7 @@ trait WikiService {
if(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
file.committer, file.time, file.commitId)
file.author, file.time, file.commitId)
}
} else None
}
@@ -182,7 +182,8 @@ trait WikiService {
}
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress,
pageName match {
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
case None => s"Revert ${from} ... ${to}"
@@ -229,7 +230,8 @@ trait WikiService {
if(created || updated || removed){
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress,
if(message.trim.length == 0) {
if(removed){
s"Rename ${currentPageName} to ${newPageName}"
@@ -269,7 +271,8 @@ trait WikiService {
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer, mailAddress, message)
}
}
}

View File

@@ -8,9 +8,11 @@ import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import util.Directory._
import util.ControlUtil._
import util.JDBCUtil._
import org.eclipse.jgit.api.Git
import util.Directory
import plugin.PluginUpdateJob
import service.SystemSettingsService
object AutoUpdate {
@@ -52,6 +54,60 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(2, 7) {
override def update(conn: Connection): Unit = {
super.update(conn)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
val repoName = rs.getString("REPOSITORY_NAME")
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
val oldDir = new File(newDir.getParentFile, "issues")
if(oldDir.exists && oldDir.isDirectory){
oldDir.renameTo(newDir)
}
}
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
val originalUserName = rs.getString("ORIGIN_USER_NAME")
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
if(originalUserName != null && originalRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
originalUserName, originalRepoName) == 0){
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
val parentUserName = rs.getString("PARENT_USER_NAME")
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
if(parentUserName != null && parentRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
parentUserName, parentRepoName) == 0){
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
}
}
},
new Version(2, 6),
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection): Unit = {
super.update(conn)
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (curInfo != newInfo) {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
FileUtils.deleteDirectory(new File(Directory.PluginHome))
}
},
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){
override def update(conn: Connection): Unit = {
@@ -61,16 +117,14 @@ object AutoUpdate {
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn)
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
}
}
@@ -93,14 +147,12 @@ object AutoUpdate {
override def update(conn: Connection): Unit = {
super.update(conn)
// Fix wiki repository configuration
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
}
}
@@ -146,24 +198,23 @@ object AutoUpdate {
*/
class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
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)
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if(dataDir != null){
System.setProperty("gitbucket.home", dataDir)
}
org.h2.Driver.load()
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
logger.debug("Start schema update")
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){
@@ -173,7 +224,6 @@ class AutoUpdateListener extends ServletContextListener {
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
}
}
@@ -184,17 +234,29 @@ class AutoUpdateListener extends ServletContextListener {
conn.rollback()
}
}
logger.debug("End schema update")
}
logger.debug("End schema update")
logger.debug("Starting plugin system...")
plugin.PluginSystem.init()
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.")
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
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 = {
@@ -207,4 +269,10 @@ class AutoUpdateListener extends ServletContextListener {
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

@@ -5,7 +5,6 @@ import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService}
import model._
import org.slf4j.LoggerFactory
import slick.jdbc.JdbcBackend
import util.Implicits._
import util.ControlUtil._
import util.Keys
@@ -67,7 +66,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
(implicit session: JdbcBackend#Session): Option[Account] =
(implicit session: Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case _ => None

View File

@@ -17,7 +17,7 @@ import WebHookService._
import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo
import service.IssuesService.IssueSearchCondition
import slick.jdbc.JdbcBackend
import model.Session
/**
* Provides Git repository via HTTP.
@@ -95,7 +95,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: JdbcBackend#Session)
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 {
@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// Retrieve all issue count in the repository
val issueCount =
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
// Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
@@ -205,7 +205,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
private def createIssueComment(commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
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")
}
}

View File

@@ -3,10 +3,13 @@ package servlet
import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils
import twirl.api.Html
import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.Account
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 {
@@ -18,7 +21,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
(req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session =>
val path = req.asInstanceOf[HttpServletRequest].getRequestURI
val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res)
}
@@ -27,50 +30,54 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
}
}
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse): Boolean = {
plugin.PluginSystem.globalActions.find(_.path == path).map { action =>
val result = action.function(request, response)
result match {
case x: String => {
response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(Html(x))
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
case x => {
// TODO returns as JSON?
response.setContentType("application/json; charset=UTF-8")
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: model.simple.Session): Boolean = {
(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("/", "/", "")
getRepository(owner, name, "").flatMap { repository => // TODO fill baseUrl
plugin.PluginSystem.repositoryActions.find(_.path == remain).map { action =>
val result = action.function(request, response)
result match {
case x: String => {
response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(x))) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
case x => {
// TODO returns as JSON?
response.setContentType("application/json; charset=UTF-8")
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
}
@@ -78,4 +85,108 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
} 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

@@ -12,7 +12,7 @@ import servlet.{Database, CommitLogHook}
import service.{AccountService, RepositoryService, SystemSettingsService}
import org.eclipse.jgit.errors.RepositoryNotFoundException
import javax.servlet.ServletContext
import model.profile.simple.Session
import model.Session
object GitCommand {
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
@@ -31,7 +31,7 @@ abstract class GitCommand(val context: ServletContext, val owner: String, val re
private def newTask(user: String): Runnable = new Runnable {
override def run(): Unit = {
Database(context) withTransaction { implicit session =>
Database(context) withSession { implicit session =>
try {
runTask(user)
callback.onExit(0)

View File

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

View File

@@ -48,7 +48,7 @@ object Directory {
* Directory for files which are attached to issue.
*/
def getAttachedDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}/issues")
new File(s"${RepositoryHome}/${owner}/${repository}/comments")
/**
* Directory for uploaded files by the specified user.

View File

@@ -0,0 +1,55 @@
package util
import java.sql._
import util.ControlUtil._
import scala.collection.mutable.ListBuffer
/**
* Provides implicit class which extends java.sql.Connection.
* This is used in automatic migration in [[servlet.AutoUpdateListener]].
*/
object JDBCUtil {
implicit class RichConnection(conn: Connection){
def update(sql: String, params: Any*): Int = {
execute(sql, params: _*){ stmt =>
stmt.executeUpdate()
}
}
def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>
val list = new ListBuffer[T]
while(rs.next){
list += f(rs)
}
list.toSeq
}
}
}
def selectInt(sql: String, params: Any*): Int = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>
if(rs.next) rs.getInt(1) else 0
}
}
}
private def execute[T](sql: String, params: Any*)(f: (PreparedStatement) => T): T = {
using(conn.prepareStatement(sql)){ stmt =>
params.zipWithIndex.foreach { case (p, i) =>
p match {
case x: Int => stmt.setInt(i + 1, x)
case x: String => stmt.setString(i + 1, x)
}
}
f(stmt)
}
}
}
}

View File

@@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git
import util.Directory._
import util.StringUtil._
import util.ControlUtil._
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._
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.errors.{ConfigInvalidException, MissingObjectException}
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 org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
@@ -47,38 +48,45 @@ object JGitUtil {
* @param id the object id
* @param isDirectory whether is it directory
* @param name the file (or directory) name
* @param time the last modified time
* @param message the last commit message
* @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 linkUrl the url of submodule
*/
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
committer: String, mailAddress: String, linkUrl: Option[String])
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String,
time: Date, author: String, mailAddress: String, linkUrl: Option[String])
/**
* The commit data.
*
* @param id the commit id
* @param time the commit time
* @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message
* @param fullMessage the full message
* @param parents the list of parent commit id
* @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,
shortMessage: String, fullMessage: String, parents: List[String]){
case class CommitInfo(id: 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(
rev.getName,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress,
rev.getShortMessage,
rev.getFullMessage,
rev.getParents().map(_.name).toList)
rev.getParents().map(_.name).toList,
rev.getAuthorIdent.getWhen,
rev.getAuthorIdent.getName,
rev.getAuthorIdent.getEmailAddress,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress)
val summary = getSummaryMessage(fullMessage, shortMessage)
@@ -87,6 +95,8 @@ object JGitUtil {
Some(fullMessage.trim.substring(i).trim)
} else None
}
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
}
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -98,7 +108,12 @@ object JGitUtil {
* @param content the string content
* @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.
@@ -176,38 +191,23 @@ object JGitUtil {
* @return HTML of the file list
*/
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 =>
val objectId = git.getRepository.resolve(revision)
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)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
treeWalk
} else {
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
treeWalk.enterSubtree()
treeWalk
}
var stopRecursive = false
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
})
}
using(treeWalk) { treeWalk =>
while (treeWalk.next()) {
// submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
@@ -216,6 +216,31 @@ object JGitUtil {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
}
list.transform(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))
}
}
}
@@ -226,11 +251,11 @@ object JGitUtil {
objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
commit.getCommitterIdent.getWhen,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName,
commit.getCommitterIdent.getName,
commit.getCommitterIdent.getEmailAddress,
commit.getAuthorIdent.getWhen,
commit.getAuthorIdent.getName,
commit.getAuthorIdent.getEmailAddress,
linkUrl)
}
}.sortWith { (file1, file2) =>
@@ -482,6 +507,17 @@ object JGitUtil {
}.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 = {
val entry = new DirCacheEntry(path)
entry.setFileMode(mode)
@@ -490,7 +526,7 @@ object JGitUtil {
}
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()
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
@@ -504,7 +540,7 @@ object JGitUtil {
inserter.flush()
inserter.release()
val refUpdate = git.getRepository.updateRef(Constants.HEAD)
val refUpdate = git.getRepository.updateRef(ref)
refUpdate.setNewObjectId(newHeadId)
refUpdate.update()
@@ -638,4 +674,15 @@ object JGitUtil {
}.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

@@ -7,6 +7,7 @@ import java.security.Security
import org.slf4j.LoggerFactory
import service.SystemSettingsService.Ldap
import scala.annotation.tailrec
import model.Account
/**
* Utility for LDAP authentication.
@@ -16,6 +17,26 @@ object LDAPUtil {
private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
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.
* Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
@@ -30,7 +51,7 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed."
){ 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 None => Left("User does not exist.")
}
@@ -47,14 +68,23 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed."
){ conn =>
findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(LDAPUserInfo(
userName = getUserNameFromMailAddress(userName),
if(ldapSettings.mailAttribute.getOrElse("").isEmpty) {
Right(LDAPUserInfo(
userName = 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.")
mailAddress = createDummyMailAddress(userName)))
} 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.
*/
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
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
if(results.hasMore){
@@ -125,7 +155,13 @@ object LDAPUtil {
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
}
}

View File

@@ -6,11 +6,11 @@ import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import app.Context
import model.Session
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database
import SystemSettingsService.Smtp
import _root_.util.ControlUtil.defining
import model.profile.simple.Session
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
@@ -28,7 +28,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
)
.distinct
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) )
}
@@ -69,8 +69,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
(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 { implicit session =>
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining(

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.
*

View File

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

View File

@@ -12,7 +12,7 @@ trait LinkConverter { self: RequestCache =>
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
value
// escape HTML tags
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
// convert issue id to link
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
getIssue(repository.owner, repository.name, m.group(2)) match {

View File

@@ -9,6 +9,7 @@ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer
import java.util.Locale
import java.util.regex.Pattern
import scala.collection.JavaConverters._
import service.{RequestCache, WikiService}
@@ -18,17 +19,23 @@ object Markdown {
* Converts Markdown of Wiki pages to HTML.
*/
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
val source = if(enableRefsLink){
val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown
// escape task list
val source = if(enableTaskList){
GitBucketHtmlSerializer.escapeTaskList(s)
} else s
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 | Extensions.SUPPRESS_ALL_HTML
).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
}
}
@@ -82,15 +89,18 @@ class GitBucketHtmlSerializer(
markdown: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean
enableRefsLink: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean
)(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url)).print("\">")
.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = {
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 = {
printer.print('<').print('a')
@@ -101,9 +111,21 @@ class GitBucketHtmlSerializer(
printer.print('>').print(rendering.text).print("</a>")
}
private def fixUrl(url: String): String = {
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){
private def fixUrl(url: String, isImage: Boolean = false): String = {
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
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 {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
}
@@ -130,7 +152,10 @@ class GitBucketHtmlSerializer(
override def visit(node: TextNode): Unit = {
// 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) {
printer.print(text)
@@ -138,6 +163,28 @@ class GitBucketHtmlSerializer(
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 {
@@ -150,4 +197,14 @@ object GitBucketHtmlSerializer {
val noSpecialChars = StringUtil.urlEncode(normalized)
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
import java.util.Date
import java.util.{Locale, Date, TimeZone}
import java.text.SimpleDateFormat
import twirl.api.Html
import play.twirl.api.Html
import util.StringUtil
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)
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'".
*/
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".
@@ -44,8 +89,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
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.
*/
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.
@@ -107,6 +152,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
)
/**

View File

@@ -1,6 +1,7 @@
@(account: model.Account, info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@import util.LDAPUtil
@html.main("Edit your profile"){
<div class="container">
<div class="row-fluid">
@@ -9,6 +10,7 @@
</div>
<div class="span9">
@helper.html.information(info)
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
<form action="@url(account.userName)/_edit" method="POST" validate="true">
<div class="box">
<div class="box-header">Profile</div>
@@ -31,7 +33,7 @@
</fieldset>
<fieldset>
<label for="mailAddress" class="strong">Mail Address:</label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.mailAddress"/>
<input type="text" name="mailAddress" id="mailAddress" value="@if(!LDAPUtil.isDummyMailAddress(account)){@account.mailAddress}"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
@@ -52,7 +54,7 @@
<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>
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn">Cancel</a>}
</div>
</div>
</div>
@@ -66,4 +68,4 @@ $(function(){
return confirm('Once you delete your account, there is no going back.\nAre you sure?');
});
});
</script>
</script>

View File

@@ -60,7 +60,7 @@ $(function(){
});
$('#addMember').click(function(){
$('#error-memberName').text('');
$('#error-members').text('');
var userName = $('#memberName').val();
// check empty
@@ -73,18 +73,18 @@ $(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
$('#error-members').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
$.post('@path/_user/existence', {
'userName': userName
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false);
} else {
$('#error-memberName').text('User does not exist.');
$('#error-members').text('User does not exist.');
}
});
});

View File

@@ -8,7 +8,7 @@
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="account-image">@avatar(account.userName, 270)</div>
<div class="account-fullname">@account.fullName</div>
<div class="account-username">@account.userName</div>
</div>

View File

@@ -25,7 +25,7 @@
@if(repository.repository.description.isDefined){
<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>
}

View File

@@ -11,9 +11,11 @@
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
<li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a>
</li>
@if(service.SystemSettingsService.enablePluginSystem){
<li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a>
</li>
}
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>

View File

@@ -31,6 +31,14 @@
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
</p>
<!--====================================================================-->
<!-- Information -->
<!--====================================================================-->
<hr>
<label><span class="strong">Information</span> (HTML is available)</label>
<fieldset>
<textarea name="information" style="width: 600px; height: 100px;">@settings.information</textarea>
</fieldset>
<!--====================================================================-->
<!-- Account registration -->
<!--====================================================================-->
<hr>
@@ -133,6 +141,13 @@
<span id="error-ldap_userNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<div class="controls">
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" value="@settings.ldap.map(_.additionalFilterCondition)"/>
<span id="error-ldap_additionalFilterCondition" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapFullNameAttribute">Full name attribute</label>
<div class="controls">

View File

@@ -59,7 +59,7 @@ $(function(){
});
$('#addMember').click(function(){
$('#error-memberName').text('');
$('#error-members').text('');
var userName = $('#memberName').val();
// check empty
@@ -72,18 +72,18 @@ $(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
$('#error-members').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
$.post('@path/_user/existence', {
'userName': userName
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false);
} else {
$('#error-memberName').text('User does not exist.');
$('#error-members').text('User does not exist.');
}
});
});

View File

@@ -16,6 +16,9 @@
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable
</label>
<div>
<span id="error-removed" class="error"></span>
</div>
}
</fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){

View File

@@ -0,0 +1,74 @@
@(openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
@closedCount Closed
</a>
</span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Visibility", flat = true){
<li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("private"))
Private repository only
</a>
</li>
<li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("public")) None else Some("public"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("public"))
Public repository only
</a>
</li>
}
@helper.html.dropdown("Organization", flat = true){
@groups.map { group =>
<li>
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
@helper.html.checkicon(condition.groups.contains(group))
@avatar(group, 20) @group
</a>
</li>
}
}
@helper.html.dropdown("Sort", flat = true){
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
}
</div>

View File

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

View File

@@ -0,0 +1,67 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
@*
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
</ul>
*@
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
@dashboard.html.header(openCount, closedCount, condition, groups)
</th>
</tr>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<img src="@assets/common/images/comment-active.png"> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 20px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>

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