Compare commits

..

147 Commits
2.0 ... 2.2.1

Author SHA1 Message Date
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
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
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
Tomofumi Tanaka
2bb1f6168a (refs #437)Show author instead of committer
* Explicit classify committer and author
* Use author to render avatar image html
* Support commit view
2014-07-29 00:06:45 +09:00
shimamoto
b13820fc0e Improved model package. The details are as follows:
* Fix the Profiles class from package object to simple object
* Fix the row case class to model package
* Define the alias of JdbcBackend#Session
2014-07-28 04:52:56 +09:00
takezoe
723de9e81e Fix TestCase 2014-07-27 22:56:02 +09:00
takezoe
3e161353ed Merge branch 'slick2-compilation-problem' 2014-07-27 21:24:40 +09:00
takezoe
2a8706630a (refs #445)Fix yen to backslash 2014-07-27 17:11:27 +09:00
Naoki Takezoe
121b6ee641 Fix incremental compilation problem caused by Slick.
This is temporary fix to decrease compilation time in development. Therefore this fix will be reverted in the future to add multiple database support capability.
2014-07-27 03:31:45 +09:00
Naoki Takezoe
34e299bf52 (refs #445)Keep line separator in online file editing 2014-07-26 23:15:47 +09:00
Naoki Takezoe
0822b7b5f3 (refs #444)Fix pull request count in dashboard 2014-07-26 19:12:09 +09:00
Naoki Takezoe
618110327a (refs #443)Fix merge guidance 2014-07-26 04:00:15 +09:00
Naoki Takezoe
f58f476060 (refs #441)Upgrade h2 to the latest version. 2014-07-25 15:46:29 +09:00
Naoki Takezoe
f5a544603a Experimental JDBC API for JavaScript plug-ins 2014-07-24 01:39:26 +09:00
Naoki Takezoe
89515cd087 Add an argument RepositoryInfo to RepositoryAction 2014-07-24 00:57:21 +09:00
Naoki Takezoe
37731c4163 Enable plugin system 2014-07-23 19:50:38 +09:00
Naoki Takezoe
1d4720d784 Merge pull request #439 from jmu/master
Fix IE copy not working
2014-07-23 01:30:21 +09:00
jmu
a10b053489 fix ie copy not working 2014-07-22 19:50:02 +08:00
takezoe
6122c8a1e1 Fix #438 2014-07-21 03:31:52 +09:00
Tomofumi Tanaka
fa9254c240 (refs #435)Correct merge commit message
Use ${user}/${branch} instead of ${user}/{repoName}
2014-07-16 23:33:14 +09:00
Tomofumi Tanaka
10616bca7d (refs #436)Encode branch name to delete at pullreq view 2014-07-16 19:24:25 +09:00
Naoki Takezoe
307f7e15e9 (refs #431)Fix CSS layout in Wiki page 2014-07-15 07:07:52 +09:00
Naoki Takezoe
86cf97d76b Fix TODO: Close issue and send webhook from online editing 2014-07-14 01:10:33 +09:00
shimamoto
01f6590c04 Fix TODO. 2014-07-14 00:08:34 +09:00
shimamoto
8f0c22bae9 Improve slick session(because transaction for unnecessary). 2014-07-13 23:54:53 +09:00
Naoki Takezoe
652a68c5b1 Fix TODO 2014-07-13 23:20:16 +09:00
Naoki Takezoe
1f56e1360d Fix font size 2014-07-13 16:31:24 +09:00
Naoki Takezoe
38475ffefe Fix avatar image size 2014-07-13 16:26:57 +09:00
Naoki Takezoe
7a44a4d726 Merge branch '401-news-feed-of-private-repo' of https://github.com/utensil/gitbucket into utensil-401-news-feed-of-private-repo
Conflicts:
	src/main/scala/app/IndexController.scala
	src/main/scala/service/ActivityService.scala
2014-07-13 16:07:42 +09:00
Naoki Takezoe
9dbc0c3fd6 Change LDAPUtil method name 2014-07-13 15:53:12 +09:00
Naoki Takezoe
56bb43ea6b AccountUtil is merged to LDAPUtil 2014-07-13 15:13:59 +09:00
Naoki Takezoe
b287c1f60d Merge branch 'add-features-to-ldapauth' of https://github.com/yjkony/gitbucket into yjkony-add-features-to-ldapauth
Conflicts:
	src/main/scala/app/IndexController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/main/scala/util/LDAPUtil.scala
	src/main/scala/util/Notifier.scala
2014-07-13 13:49:04 +09:00
Naoki Takezoe
258d53b7a6 Disable submit buttons while performing validation 2014-07-13 03:48:44 +09:00
Naoki Takezoe
2e11d6dd78 Disable submit buttons while performing validation 2014-07-13 03:47:40 +09:00
Tomofumi Tanaka
a2a2e22485 Fix compilation error service test case 2014-07-09 00:25:40 +09:00
Tomofumi Tanaka
c182cde14b Revert "Disable TestCase for Services"
This reverts commit 104c3bc89d.
2014-07-08 23:55:15 +09:00
Naoki Takezoe
104c3bc89d Disable TestCase for Services 2014-07-06 17:35:18 +09:00
Naoki Takezoe
2668977918 Convert CRLF to LF 2014-07-06 17:07:52 +09:00
Naoki Takezoe
28424c96c4 Readying 2.1 release 2014-07-06 17:06:26 +09:00
Naoki Takezoe
9cfa8c594b Merge branch 'master' into slick2
Conflicts:
	project/build.scala
	src/main/scala/app/IndexController.scala
	src/main/scala/app/RepositorySettingsController.scala
	src/main/scala/model/Account.scala
	src/main/scala/model/BasicTemplate.scala
	src/main/scala/model/Issue.scala
	src/main/scala/model/IssueComment.scala
	src/main/scala/model/package.scala
	src/main/scala/service/IssuesService.scala
	src/main/scala/service/PullRequestService.scala
	src/main/scala/service/RepositoryService.scala
	src/main/scala/service/WikiService.scala
	src/main/scala/servlet/TransactionFilter.scala
	src/main/scala/util/Notifier.scala
2014-07-06 17:02:49 +09:00
Naoki Takezoe
5c70cd654c (refs #341)Add TODO about Slick 2.0 migration 2014-07-06 16:21:25 +09:00
Naoki Takezoe
7aca24e51d (refs #341)Fix compilation error about date conversion 2014-07-06 16:09:50 +09:00
Naoki Takezoe
cce0b67871 (refs #341)Fix compilation error of delete statements 2014-07-06 15:42:45 +09:00
Naoki Takezoe
606cd83f44 Fix CRLF to LF 2014-07-06 13:19:27 +09:00
Naoki Takezoe
32897c36f9 (refs #341)Fix compilation error other than date mapping and delete statement 2014-07-06 13:07:05 +09:00
Naoki Takezoe
92e4e12655 Update README.md 2014-07-05 18:06:51 +09:00
Naoki Takezoe
c8e5b75165 Disable front-end of plugin system in GitBucket 2.1 2014-07-05 18:01:19 +09:00
Naoki Takezoe
09b9a52ad3 Merge branch 'plugin-system' 2014-07-05 17:36:02 +09:00
Naoki Takezoe
33378c6464 Merge pull request #425 from okapies/fix-ldap-filter
Filter by username explicitly
2014-07-05 11:54:08 +09:00
shimamoto
259bcfc14f (refs #341) Fix compile errors. 2014-07-01 04:08:15 +09:00
shimamoto
c361d24ba4 (refs #341) Implement a method to get the session. 2014-07-01 03:43:55 +09:00
shimamoto
d5e1b18b52 (refs #341) Sets default value. 2014-07-01 03:40:21 +09:00
tanacasino
684a17a15b Merge pull request #422 from tanacasino/improve-markdown-css
Make markdown looks like more GitHub
2014-07-01 02:44:11 +09:00
Yuta Okamoto
66b7b69d20 specify LDAP search filter explicitly 2014-06-30 23:38:43 +09:00
Tomofumi Tanaka
57254f6366 Make markdown looks like more GitHub 2014-06-29 22:17:12 +09:00
Naoki Takezoe
c64909ab1a Plugin updating is executed asynchronously by Quartz Scheduler 2014-06-29 16:09:41 +09:00
Naoki Takezoe
34dd8541f4 Disable JavaScript console 2014-06-29 14:35:49 +09:00
Naoki Takezoe
50b4fb154d Fix JavaScript path 2014-06-29 14:33:48 +09:00
Naoki Takezoe
0b3781ec8a Add plugin updating capability 2014-06-29 14:26:33 +09:00
Naoki Takezoe
0e1d184715 Remove installed plugins from available plugin list 2014-06-29 12:39:27 +09:00
Naoki Takezoe
d8c27046f6 Merge branch 'master' into plugin-system
Conflicts:
	src/main/scala/servlet/AutoUpdateListener.scala
2014-06-29 04:20:58 +09:00
Naoki Takezoe
fd09058a7d (refs #310)Convert all CRLF to LF 2014-06-29 04:16:37 +09:00
Naoki Takezoe
1c99b57709 (refs #412)Fix repository lock 2014-06-29 03:29:03 +09:00
shimamoto
9ee739d102 (refs #341) Migrate slick session. 2014-06-27 08:48:58 +09:00
Tomofumi Tanaka
e2cde81b72 (refs #417) Correct wiki repository url 2014-06-25 23:40:41 +09:00
Tomofumi Tanaka
84a4b8fd92 (refs #415) Fix bug 2014-06-25 00:19:28 +09:00
shimamoto
d2c94909cb (refs #341) Migrate service package. 2014-06-24 02:40:40 +09:00
utensil
3683a5fb7d Show newsfeed of private repo to members of owner group 2014-06-22 09:59:59 +00:00
utensil
1223bf2fd8 Show newsfeed of private repo to its owner 2014-06-22 08:10:37 +00:00
Naoki Takezoe
a9bfe0dfab (refs #411)Move thirdparty JavaScript and CSS to vendors/ 2014-06-20 11:51:27 +09:00
Naoki Takezoe
9af81c7093 Merge pull request #410 from cranst0n/ant-deprecation
Remove deprecated Ant 'rename' task.
2014-06-20 09:25:43 +09:00
cranst0n
1e8a5c3cde Remove deprecated 'rename' task. 2014-06-19 13:21:24 -04:00
Naoki Takezoe
707ad866e1 (refs #408)Performance improvement for index page. 2014-06-20 01:42:42 +09:00
Naoki Takezoe
c3a944b40e (refs #405)Fix styles 2014-06-16 10:59:21 +09:00
Naoki Takezoe
ab80cb8f60 (refs #405)Fix styles 2014-06-16 10:39:27 +09:00
Naoki Takezoe
4f45e047d2 (refs #406)Fix pull request count in the dashboard 2014-06-16 01:40:51 +09:00
shimamoto
bbe455ac49 (refs #341) Migrate model package. 2014-06-16 01:29:56 +09:00
Naoki Takezoe
b5f173fa46 (refs #32)Display plugin's status 2014-06-16 01:20:42 +09:00
Naoki Takezoe
4bd6ef143a (refs #32)Clone or pull plugin repository before displaying the available plugins page 2014-06-15 18:07:34 +09:00
Naoki Takezoe
fd4a696303 (refs #32)Add version to plugin meta information 2014-06-15 17:39:42 +09:00
Naoki Takezoe
4af4c4e7c6 (refs #32)Add plugin install tab 2014-06-15 13:11:08 +09:00
Naoki Takezoe
3b2e42fd61 (refs #32)Making plugin administration pages 2014-06-14 23:29:37 +09:00
Naoki Takezoe
b07d0b028f (refs #32)Add deleting installed plugins 2014-06-14 14:32:40 +09:00
Naoki Takezoe
f3900ca8f9 (refs #32)Add plugin system initialization 2014-06-14 14:00:21 +09:00
Naoki Takezoe
62d43f120a (refs #32)Separate Plugin interface and implementation 2014-06-14 13:26:56 +09:00
Naoki Takezoe
c4f69fbd13 (refs #32)Switch JavaScript processor to Rhino from Nashorn because GitBucket should work on both of JDK7 and JDK8 2014-06-14 10:58:58 +09:00
Naoki Takezoe
fece20ff40 (refs #32)Implementing repository action processing 2014-06-11 01:45:51 +09:00
Naoki Takezoe
bbef4b22ca (refs #32)Add plugins page into the system admin tools 2014-06-10 10:44:36 +09:00
Naoki Takezoe
481a2d213f (refs #32)Improving plug-in API 2014-06-10 07:43:52 +09:00
Tomofumi Tanaka
8ed4075f1e Change word to clear milestone the same as GitHub 2014-06-10 00:40:09 +09:00
Tomofumi Tanaka
9bf82733d1 Make icon-remove-circle of dropdown menu unclickable 2014-06-10 00:24:30 +09:00
Tomofumi Tanaka
30d66f95bc Show number of conversations 2014-06-09 22:49:41 +09:00
Naoki Takezoe
378c2c39a8 (refs #32)Add JavaScript API 2014-06-08 00:37:01 +09:00
Naoki Takezoe
daf5fc434c Merge remote-tracking branch 'origin/plugin-system' into plugin-system 2014-06-07 18:53:31 +09:00
Naoki Takezoe
e5bf90ed26 (refs #32)Provide generic layout and context to custom actions 2014-06-07 18:53:07 +09:00
Tomofumi Tanaka
1bf3146220 (refs #396)Apply syntax highlight correctly when update comment 2014-06-06 22:22:51 +09:00
Naoki Takezoe
ddd51850f0 (refs #32)Add JavaScript Console 2014-06-06 17:20:48 +09:00
Naoki Takezoe
e14a0c3770 (refs #32)Enable menu icon which is injected by plug-in 2014-06-06 16:35:54 +09:00
Naoki Takezoe
b0b318ce30 (refs #32)Example of custom action extension 2014-06-05 21:22:16 +09:00
Naoki Takezoe
6f666ca49f Merge branch 'master' into plugin-system 2014-06-05 20:54:28 +09:00
Naoki Takezoe
0cb2116bdf (refs #32)First impression of the plugin system 2014-06-05 20:52:38 +09:00
Naoki Takezoe
280113497b Merge pull request #390 from sakapoko/navform
Fix nested tags.
2014-06-05 09:37:40 +09:00
Shuji Sakagami
5f6e318329 Fix nested tags. 2014-06-04 13:11:37 +09:00
takezoe
f8921b6f10 Add a badge which shows number of pages 2014-06-03 07:28:30 +09:00
Naoki Takezoe
31a08abff2 (refs #378)"Delete branch" button is displayed for only merged pull requests 2014-06-02 21:34:03 +09:00
Naoki Takezoe
0fa1e11c5a (refs #378)Closed but not merged pull requests should be re-openable 2014-06-02 21:28:20 +09:00
yjkony
e2c99a46be Merge commit Tag 2.0 'db5395ddbc4aef485415408720dd09cfc215b527' into add-features-to-ldapauth
Conflicts:
	src/main/twirl/account/edit.scala.html
2014-06-02 17:01:22 +09:00
Naoki Takezoe
1edff41690 Fix code style 2014-06-02 16:10:03 +09:00
Naoki Takezoe
6d6f529d40 (refs #380)Close stream certainly 2014-06-02 16:04:45 +09:00
Naoki Takezoe
e2fd7d9d8e Fix "New pull request" button style 2014-06-01 23:46:28 +09:00
Naoki Takezoe
61146687b3 Merge remote-tracking branch 'origin/master' 2014-06-01 22:58:24 +09:00
Naoki Takezoe
1d1f7fa581 (refs #382)Remove unnecessary comment 2014-06-01 22:58:07 +09:00
Tomofumi Tanaka
67da88fab5 Use "Conversation" instead of "Discussion"
Github uses "Conversation" now.
2014-06-01 22:45:10 +09:00
Naoki Takezoe
fb3ed70215 (refs #382)Exclude duplicated commits from applying issue comment 2014-06-01 21:47:45 +09:00
Naoki Takezoe
2fceeeee4e Merge pull request #386 from HairyFotr/patch-4
Small cleanup using static analysis
2014-05-31 17:26:23 +09:00
Naoki Takezoe
67102822e8 Update README.md 2014-05-31 13:17:21 +09:00
Naoki Takezoe
d00a0f1571 Update README.md 2014-05-31 13:16:53 +09:00
HairyFotr
7698f12112 Small cleanup using static analysis 2014-05-31 00:57:03 +02: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
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
355 changed files with 100900 additions and 63512 deletions

View File

@@ -80,6 +80,24 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.2 - 4 Aug 2014
- Plug-in system is available
- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
- tar.gz export for repository contents
- LDAP authentication improvement (mail address became optional)
- Show news feed of a private repository to members
- Some bug fix and improvements
### 2.1 - 6 Jul 2014
- Upgrade to Slick 2.0 from 1.9
- Base part of the plug-in system is merged
- Many bug fix and improvements
### 2.0 - 31 May 2014
- Modern Github UI
- Preview in AceEditor
- Select lines by clicking line number in blob view
### 1.13 - 29 Apr 2014 ### 1.13 - 29 Apr 2014
- Direct file editing in the repository viewer using AceEditor - Direct file editing in the repository viewer using AceEditor
- File attachment for issues - File attachment for issues

View File

@@ -4,7 +4,7 @@
<property name="target.dir" value="target"/> <property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/> <property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/> <property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.10"/> <property name="scala.version" value="2.11"/>
<property name="gitbucket.version" value="0.0.1"/> <property name="gitbucket.version" value="0.0.1"/>
<property name="jetty.version" value="8.1.8.v20121106"/> <property name="jetty.version" value="8.1.8.v20121106"/>
<property name="servlet.version" value="3.0.0.v201112011016"/> <property name="servlet.version" value="3.0.0.v201112011016"/>
@@ -50,8 +50,8 @@
</target> </target>
<target name="rename" depends="embed"> <target name="rename" depends="embed">
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war" <move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/> tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target> </target>
<target name="all" depends="rename"> <target name="all" depends="rename">

View File

@@ -8,8 +8,8 @@ object MyBuild extends Build {
val Organization = "jp.sf.amateras" val Organization = "jp.sf.amateras"
val Name = "gitbucket" val Name = "gitbucket"
val Version = "0.0.1" val Version = "0.0.1"
val ScalaVersion = "2.10.3" val ScalaVersion = "2.11.2"
val ScalatraVersion = "2.2.1" val ScalatraVersion = "2.3.0"
lazy val project = Project ( lazy val project = Project (
"gitbucket", "gitbucket",
@@ -26,21 +26,24 @@ object MyBuild extends Build {
), ),
scalacOptions := Seq("-deprecation", "-language:postfixOps"), scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.5", "org.json4s" %% "json4s-jackson" % "3.2.10",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.14", "jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4", "commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1", "org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5", "org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1", "org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3", "org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.11.0", "org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "1.0.1", "com.typesafe.slick" %% "slick" % "2.1.0-RC3",
"org.mozilla" % "rhino" % "1.7R4",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.173", "org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"), "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),

View File

@@ -1,4 +1,4 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter} import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
import app._ import app._
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider //import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._ import org.scalatra._
@@ -10,6 +10,8 @@ class ScalatraBootstrap extends LifeCycle {
// Register TransactionFilter and BasicAuthenticationFilter at first // Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter) context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter) context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")

View File

@@ -5,6 +5,7 @@ import util._
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._
import ssh.SshUtil import ssh.SshUtil
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
@@ -291,7 +292,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){ LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
@@ -354,7 +355,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){ if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
// redirect to the repository if repository already exists // redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}") redirect(s"/${loginUserName}/${repository.name}")

View File

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

View File

@@ -49,21 +49,21 @@ trait DashboardControllerBase extends ControllerBase {
) )
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
//
dashboard.html.issues( dashboard.html.issues(
issues.html.listparts( issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*), searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page, page,
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*), countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*), countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
condition), condition),
countIssue(condition, Map.empty, false, repositories: _*), countIssue(condition, Map.empty, false, userRepos: _*),
countIssue(condition, Map("assigned" -> userName), false, repositories: _*), countIssue(condition, Map("assigned" -> userName), false, userRepos: _*),
countIssue(condition, Map("created_by" -> userName), false, repositories: _*), countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
countIssueGroupByRepository(condition, filterUser, false, repositories: _*), countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
condition, condition,
filter) filter)
@@ -80,25 +80,26 @@ trait DashboardControllerBase extends ControllerBase {
}.copy(repo = repository)) }.copy(repo = repository))
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) val allRepos = getAllRepositories()
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository( val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*) IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*)
dashboard.html.pulls( dashboard.html.pulls(
pulls.html.listparts( pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*), searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page, page,
countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*), countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*), countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
condition, condition,
None, None,
false), false),
getPullRequestCountGroupByUser(condition.state == "closed", userName, None), getPullRequestCountGroupByUser(condition.state == "closed", None, None),
getRepositoryNamesOfUser(userName).map { RepoName => userRepos.map { case (userName, repoName) =>
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0)) (userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
}.sortBy(_._3).reverse, }.sortBy(_._3).reverse,
condition, condition,
filter) filter)

View File

@@ -1,6 +1,7 @@
package app package app
import util._ import util._
import util.Implicits._
import service._ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
@@ -19,11 +20,23 @@ trait IndexControllerBase extends ControllerBase {
get("/"){ get("/"){
val loginAccount = context.loginAccount val loginAccount = context.loginAccount
if(loginAccount.isEmpty) {
html.index(getRecentActivities(), html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl), getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil) 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(getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
}
} }
get("/signin"){ get("/signin"){
@@ -58,8 +71,12 @@ trait IndexControllerBase extends ControllerBase {
session.setAttribute(Keys.Session.LoginAccount, account) session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName) updateLastLoginDate(account.userName)
if(LDAPUtil.isDummyMailAddress(account)) {
redirect("/" + account.userName + "/_edit")
}
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){ if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/") redirect("/")
} else { } else {
redirect(redirectUrl) redirect(redirectUrl)
@@ -71,8 +88,6 @@ trait IndexControllerBase extends ControllerBase {
/** /**
* JSON API for collaborator completion. * JSON API for collaborator completion.
*
* TODO Move to other controller?
*/ */
get("/_user/proposals")(usersOnly { get("/_user/proposals")(usersOnly {
contentType = formats("json") contentType = formats("json")
@@ -81,5 +96,11 @@ trait IndexControllerBase extends ControllerBase {
) )
}) })
/**
* JSON APU for checking user existence.
*/
post("/_user/existence")(usersOnly {
getAccountByUserName(params("userName")).isDefined
})
} }

View File

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

View File

@@ -13,7 +13,6 @@ import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._ import service.IssuesService._
import service.PullRequestService._ import service.PullRequestService._
import util.JGitUtil.DiffInfo import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
@@ -124,7 +123,7 @@ trait PullRequestsControllerBase extends ControllerBase {
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){ LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) => getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git => using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close. // mark issue as merged and close.
@@ -157,7 +156,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
mergeCommit.setAuthor(personIdent) mergeCommit.setAuthor(personIdent)
mergeCommit.setCommitter(personIdent) mergeCommit.setCommitter(personIdent)
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n\n" + mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" +
form.message) form.message)
// insertObject and got mergeCommit Object Id // insertObject and got mergeCommit Object Id
@@ -367,7 +366,7 @@ trait PullRequestsControllerBase extends ControllerBase {
*/ */
private def checkConflict(userName: String, repositoryName: String, branch: String, private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){ LockUtil.lock(s"${userName}/${repositoryName}"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}" val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}" val tmpRefName = s"refs/merge-check/${userName}/${branch}"
@@ -403,7 +402,7 @@ trait PullRequestsControllerBase extends ControllerBase {
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String, private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = { issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge") { LockUtil.lock(s"${userName}/${repositoryName}") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git => using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge // merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
@@ -444,7 +443,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit) new CommitInfo(revCommit)
}.toList.splitWith { (commit1, commit2) => }.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
} }
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
@@ -466,7 +465,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.list( pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)), getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
userName, userName,
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),

View File

@@ -2,7 +2,10 @@ package app
import service._ import service._
import util.Directory._ import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator} import util.ControlUtil._
import util.Implicits._
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import util.JGitUtil.CommitInfo
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
@@ -185,6 +188,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
// Change repository owner // Change repository owner
if(repository.owner != form.newOwner){ if(repository.owner != form.newOwner){
LockUtil.lock(s"${repository.owner}/${repository.name}"){
// Update database // Update database
renameRepository(repository.owner, repository.name, form.newOwner, repository.name) renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Move git repository // Move git repository
@@ -196,6 +200,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
} }
} }
}
redirect(s"/${form.newOwner}/${repository.name}") redirect(s"/${form.newOwner}/${repository.name}")
}) })
@@ -203,12 +208,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Delete the repository. * Delete the repository.
*/ */
post("/:owner/:repository/settings/delete")(ownerOnly { repository => post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}"){
deleteRepository(repository.owner, repository.name) deleteRepository(repository.owner, repository.name)
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
}
redirect(s"/${repository.owner}") redirect(s"/${repository.owner}")
}) })

View File

@@ -8,23 +8,31 @@ import _root_.util._
import service._ import service._
import org.scalatra._ import org.scalatra._
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} import org.eclipse.jgit.revwalk.RevCommit
import service.WebHookService.WebHookPayload
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator
/** /**
* The repository viewer. * The repository viewer.
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator => self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class EditorForm( case class EditorForm(
branch: String, branch: String,
@@ -32,6 +40,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
content: String, content: String,
message: Option[String], message: Option[String],
charset: String, charset: String,
lineSeparator: String,
newFileName: String, newFileName: String,
oldFileName: Option[String] oldFileName: Option[String]
) )
@@ -49,6 +58,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"content" -> trim(label("Content", text(required))), "content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))), "message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))), "charset" -> trim(label("Charset", text(required))),
"lineSeparator" -> trim(label("Line Separator", text(required))),
"newFileName" -> trim(label("Filename", text(required))), "newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text()))) "oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply) )(EditorForm.apply)
@@ -101,7 +111,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext) }, page, hasNext)
case Left(_) => NotFound case Left(_) => NotFound
} }
@@ -142,7 +152,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), None, form.content, form.charset, commitFile(repository, form.branch, form.path, Some(form.newFileName), None,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
form.message.getOrElse(s"Create ${form.newFileName}")) form.message.getOrElse(s"Create ${form.newFileName}"))
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${ redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
@@ -151,7 +162,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName, form.content, form.charset, commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
if(form.oldFileName.exists(_ == form.newFileName)){ if(form.oldFileName.exists(_ == form.newFileName)){
form.message.getOrElse(s"Update ${form.newFileName}") form.message.getOrElse(s"Update ${form.newFileName}")
} else { } else {
@@ -252,50 +264,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Download repository contents as an archive. * Download repository contents as an archive.
*/ */
get("/:owner/:repository/archive/*")(referrersOnly { repository => get("/:owner/:repository/archive/*")(referrersOnly { repository =>
val name = multiParams("splat").head multiParams("splat").head match {
case name if name.endsWith(".zip") =>
if(name.endsWith(".zip")){ archiveRepository(name, ".zip", repository)
val revision = name.replaceFirst("\\.zip$", "") case name if name.endsWith(".tar.gz") =>
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) archiveRepository(name, ".tar.gz", repository)
if(workDir.exists){ case _ => BadRequest
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
val zipFile = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + ".zip")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new TreeWalk(git.getRepository)){ walk =>
val reader = walk.getObjectReader
val objectId = new MutableObjectId
using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
entry.setSize(loader.getSize)
out.putNextEntry(entry)
loader.copyTo(out)
}
}
}
}
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile
} else {
BadRequest
} }
}) })
@@ -316,9 +290,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case branch if(path == branch || path.startsWith(branch + "/")) => branch case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst { } orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} orElse Some(path.split("/")(0)) get } getOrElse path.split("/")(0)
(id, path.substring(id.length).replaceFirst("^/", "")) (id, path.substring(id.length).stripPrefix("/"))
} }
@@ -408,8 +382,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
// TODO invoke hook // close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call web hook
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
}
case _ =>
}
} }
} }
} }
@@ -429,4 +414,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
} }
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
val file = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new java.io.FileOutputStream(file)) { out =>
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(out)
.call()
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
file
}
}
} }

View File

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

View File

@@ -3,8 +3,14 @@ package app
import service.{AccountService, SystemSettingsService} import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import ssh.SshServer import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
import org.scalatra.Ok
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator with AccountService with AdminAuthenticator
@@ -36,8 +42,9 @@ trait SystemSettingsControllerBase extends ControllerBase {
"bindPassword" -> trim(label("Bind Password", optional(text()))), "bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))), "baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))), "userNameAttribute" -> trim(label("User name attribute", text(required))),
"additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))), "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", text(required))), "mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))), "tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text()))) "keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply)) )(Ldap.apply))
@@ -47,6 +54,11 @@ trait SystemSettingsControllerBase extends ControllerBase {
} else Nil } else Nil
} }
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
admin.html.system(flash.get("info")) admin.html.system(flash.get("info"))
@@ -71,4 +83,103 @@ trait SystemSettingsControllerBase extends ControllerBase {
redirect("/admin/system") redirect("/admin/system")
}) })
get("/admin/plugins")(adminOnly {
val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
})
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
deletePlugins(form.pluginIds)
installPlugins(form.pluginIds)
redirect("/admin/plugins")
})
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
deletePlugins(form.pluginIds)
redirect("/admin/plugins")
})
get("/admin/plugins/available")(adminOnly {
val installedPlugins = plugin.PluginSystem.plugins
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins)
})
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
installPlugins(form.pluginIds)
redirect("/admin/plugins")
})
// get("/admin/plugins/console")(adminOnly {
// admin.plugins.html.console()
// })
//
// post("/admin/plugins/console")(adminOnly {
// val script = request.getParameter("script")
// val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
// Ok(result)
// })
// TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = {
pluginIds.foreach { pluginId =>
plugin.PluginSystem.uninstall(pluginId)
val dir = new java.io.File(PluginHome, pluginId)
if(dir.exists && dir.isDirectory){
FileUtils.deleteQuietly(dir)
PluginSystem.uninstall(pluginId)
}
}
}
private def installPlugins(pluginIds: List[String]): Unit = {
val dir = getPluginCacheDir()
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(!pluginDir.exists){
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
}
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
} }

View File

@@ -4,10 +4,11 @@ import service._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._ import util.ControlUtil._
import util.Directory._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.Directory._
class UserManagementController extends UserManagementControllerBase class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
@@ -181,11 +182,6 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} }
}) })
// TODO Move to other generic controller?
post("/admin/users/_usercheck"){
getAccountByUserName(params("userName")).isDefined
}
private def members: Constraint = new Constraint(){ private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists { if(value.split(",").exists {

View File

@@ -4,10 +4,10 @@ import service._
import util._ import util._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import scala.Some
import java.util.ResourceBundle import java.util.ResourceBundle
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase

View File

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

View File

@@ -1,25 +1,29 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait ActivityComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate { lazy val Activities = TableQuery[Activities]
def activityId = column[Int]("ACTIVITY_ID", O AutoInc)
def activityUserName = column[String]("ACTIVITY_USER_NAME") class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
def activityType = column[String]("ACTIVITY_TYPE") val activityId = column[Int]("ACTIVITY_ID", O AutoInc)
def message = column[String]("MESSAGE") val activityUserName = column[String]("ACTIVITY_USER_NAME")
def additionalInfo = column[String]("ADDITIONAL_INFO") val activityType = column[String]("ACTIVITY_TYPE")
def activityDate = column[java.util.Date]("ACTIVITY_DATE") val message = column[String]("MESSAGE")
def * = activityId ~ userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate <> (Activity, Activity.unapply _) val additionalInfo = column[String]("ADDITIONAL_INFO")
def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId val activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
}
} }
case class Activity( case class Activity(
activityId: Int,
userName: String, userName: String,
repositoryName: String, repositoryName: String,
activityUserName: String, activityUserName: String,
activityType: String, activityType: String,
message: String, message: String,
additionalInfo: Option[String], additionalInfo: Option[String],
activityDate: java.util.Date activityDate: java.util.Date,
activityId: Int = 0
) )

View File

@@ -1,44 +1,47 @@
package model package model
import scala.slick.driver.H2Driver.simple._ protected[model] trait TemplateComponent { self: Profile =>
import profile.simple._
protected[model] trait BasicTemplate { self: Table[_] => trait BasicTemplate { self: Table[_] =>
def userName = column[String]("USER_NAME") val userName = column[String]("USER_NAME")
def repositoryName = column[String]("REPOSITORY_NAME") val repositoryName = column[String]("REPOSITORY_NAME")
def byRepository(owner: String, repository: String) = 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]) = def byRepository(userName: Column[String], repositoryName: Column[String]) =
(this.userName is userName) && (this.repositoryName is repositoryName) (this.userName === userName) && (this.repositoryName === repositoryName)
} }
protected[model] trait IssueTemplate extends BasicTemplate { self: Table[_] => trait IssueTemplate extends BasicTemplate { self: Table[_] =>
def issueId = column[Int]("ISSUE_ID") val issueId = column[Int]("ISSUE_ID")
def byIssue(owner: String, repository: String, issueId: Int) = 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]) = def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
byRepository(userName, repositoryName) && (this.issueId is issueId) byRepository(userName, repositoryName) && (this.issueId === issueId)
} }
protected[model] trait LabelTemplate extends BasicTemplate { self: Table[_] => trait LabelTemplate extends BasicTemplate { self: Table[_] =>
def labelId = column[Int]("LABEL_ID") val labelId = column[Int]("LABEL_ID")
def byLabel(owner: String, repository: String, labelId: Int) = 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]) = def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
byRepository(userName, repositoryName) && (this.labelId is labelId) byRepository(userName, repositoryName) && (this.labelId === labelId)
} }
protected[model] trait MilestoneTemplate extends BasicTemplate { self: Table[_] => trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
def milestoneId = column[Int]("MILESTONE_ID") val milestoneId = column[Int]("MILESTONE_ID")
def byMilestone(owner: String, repository: String, milestoneId: Int) = 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]) = def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
byRepository(userName, repositoryName) && (this.milestoneId is milestoneId) byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
}
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,20 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait IssueLabelComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object IssueLabels extends Table[IssueLabel]("ISSUE_LABEL") with IssueTemplate with LabelTemplate { lazy val IssueLabels = TableQuery[IssueLabels]
def * = userName ~ repositoryName ~ issueId ~ labelId <> (IssueLabel, IssueLabel.unapply _)
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) = 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( case class IssueLabel(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
issueId: Int, issueId: Int,
labelId: Int) labelId: Int
)

View File

@@ -1,21 +1,25 @@
package model package model
import scala.slick.driver.H2Driver.simple._ trait LabelComponent extends TemplateComponent { self: Profile =>
import profile.simple._
object Labels extends Table[Label]("LABEL") with LabelTemplate { lazy val Labels = TableQuery[Labels]
def labelName = column[String]("LABEL_NAME")
def color = column[String]("COLOR") class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate {
def * = userName ~ repositoryName ~ labelId ~ labelName ~ color <> (Label, Label.unapply _) override val labelId = column[Int]("LABEL_ID", O AutoInc)
val labelName = column[String]("LABEL_NAME")
val color = column[String]("COLOR")
def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)
def ins = userName ~ repositoryName ~ labelName ~ color
def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) 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) def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
}
} }
case class Label( case class Label(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
labelId: Int, labelId: Int = 0,
labelName: String, labelName: String,
color: String){ color: String){
@@ -30,5 +34,4 @@ case class Label(
"FFFFFF" "FFFFFF"
} }
} }
} }

View File

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

View File

@@ -0,0 +1,41 @@
package model
trait Profile {
val profile: slick.driver.JdbcProfile
import profile.simple._
// java.util.Date Mapped Column Types
implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
)
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
}
object Profile extends {
val profile = slick.driver.H2Driver
} with AccountComponent
with ActivityComponent
with CollaboratorComponent
with GroupMemberComponent
with IssueComponent
with IssueCommentComponent
with IssueLabelComponent
with LabelComponent
with MilestoneComponent
with PullRequestComponent
with RepositoryComponent
with SshKeyComponent
with WebHookComponent with Profile {
/**
* Returns system date.
*/
def currentDate = new java.util.Date()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,117 @@
package plugin
import org.mozilla.javascript.{Context => JsContext}
import org.mozilla.javascript.{Function => JsFunction}
import scala.collection.mutable.ListBuffer
import plugin.PluginSystem._
import util.ControlUtil._
import plugin.PluginSystem.GlobalMenu
import plugin.PluginSystem.RepositoryAction
import plugin.PluginSystem.Action
import plugin.PluginSystem.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[RepositoryAction]()
private val globalActionList = ListBuffer[Action]()
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 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 += RepositoryAction(path, (request, response, repository) => {
val context = JsContext.enter()
try {
function.call(context, function, function, Array(request, response, repository))
} finally {
JsContext.exit()
}
})
}
object db {
// TODO Use JavaScript Map instead of java.util.Map
def select(sql: String): Array[java.util.Map[String, String]] = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
using(stmt.executeQuery()){ rs =>
val list = new java.util.ArrayList[java.util.Map[String, String]]()
while(rs.next){
defining(rs.getMetaData){ meta =>
val map = new java.util.HashMap[String, String]()
Range(1, meta.getColumnCount).map { i =>
val name = meta.getColumnName(i)
map.put(name, rs.getString(name))
}
list.add(map)
}
}
list.toArray(new Array[java.util.Map[String, String]](list.size))
}
}
}
}
}
}
object JavaScriptPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new JavaScriptPlugin(id, version, author, url, description)
def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = {
val context = JsContext.enter()
try {
val scope = context.initStandardObjects()
scope.put("PluginSystem", scope, PluginSystem)
scope.put("JavaScriptPlugin", scope, this)
vars.foreach { case (key, value) =>
scope.put(key, scope, value)
}
val result = context.evaluateString(scope, script, "<cmd>", 1, null)
result
} finally {
JsContext.exit
}
}
}

View File

@@ -0,0 +1,21 @@
package plugin
import plugin.PluginSystem._
import java.sql.Connection
trait Plugin {
val id: String
val version: String
val author: String
val url: String
val description: String
def repositoryMenus : List[RepositoryMenu]
def globalMenus : List[GlobalMenu]
def repositoryActions : List[RepositoryAction]
def globalActions : List[Action]
}
object PluginConnectionHolder {
val threadLocal = new ThreadLocal[Connection]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,33 @@
package service package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import scala.slick.jdbc.{StaticQuery => Q} import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation import Q.interpolation
import model._ import model.Profile._
import profile.simple._
import model.{Issue, IssueComment, IssueLabel, Label}
import util.Implicits._ import util.Implicits._
import util.StringUtil._ import util.StringUtil._
trait IssuesService { trait IssuesService {
import IssuesService._ import IssuesService._
def getIssue(owner: String, repository: String, issueId: String) = def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
if (issueId forall (_.isDigit)) if (issueId forall (_.isDigit))
Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
else None else None
def getComments(owner: String, repository: String, issueId: Int) = def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list IssueComments filter (_.byIssue(owner, repository, issueId)) list
def getComment(owner: String, repository: String, commentId: String) = def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
if (commentId forall (_.isDigit)) if (commentId forall (_.isDigit))
Query(IssueComments) filter { t => IssueComments filter { t =>
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
} firstOption } firstOption
else None else None
def getIssueLabels(owner: String, repository: String, issueId: Int) = def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) =
IssueLabels IssueLabels
.innerJoin(Labels).on { (t1, t2) => .innerJoin(Labels).on { (t1, t2) =>
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
@@ -36,8 +36,8 @@ trait IssuesService {
.map ( _._2 ) .map ( _._2 )
.list .list
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
/** /**
* Returns the count of the search result against issues. * Returns the count of the search result against issues.
@@ -49,8 +49,9 @@ trait IssuesService {
* @return the count of the search result * @return the count of the search result
*/ */
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): Int = repos: (String, String)*)(implicit s: Session): Int =
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
/** /**
* Returns the Map which contains issue count for each labels. * Returns the Map which contains issue count for each labels.
* *
@@ -61,7 +62,7 @@ trait IssuesService {
* @return the Map which contains issue count for each labels (key is label name, value is issue count) * @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/ */
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String]): Map[String, Int] = { 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), filterUser, false)
.innerJoin(IssueLabels).on { (t1, t2) => .innerJoin(IssueLabels).on { (t1, t2) =>
@@ -74,7 +75,7 @@ trait IssuesService {
t3.labelName t3.labelName
} }
.map { case (labelName, t) => .map { case (labelName, t) =>
labelName ~ t.length labelName -> t.length
} }
.toMap .toMap
} }
@@ -90,13 +91,13 @@ trait IssuesService {
*/ */
def countIssueGroupByRepository( def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): List[(String, String, Int)] = { repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
.groupBy { t => .groupBy { t =>
t.userName ~ t.repositoryName t.userName -> t.repositoryName
} }
.map { case (repo, t) => .map { case (repo, t) =>
repo ~ t.length (repo._1, repo._2, t.length)
} }
.sortBy(_._3 desc) .sortBy(_._3 desc)
.list .list
@@ -114,7 +115,8 @@ trait IssuesService {
* @return the search result (list of tuples which contain issue, labels and comment count) * @return the search result (list of tuples which contain issue, labels and comment count)
*/ */
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = { offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, List[Label], Int)] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, onlyPullRequest) searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
@@ -157,20 +159,20 @@ trait IssuesService {
* Assembles query for conditional issue searching. * Assembles query for conditional issue searching.
*/ */
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean) = filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
Query(Issues) filter { t1 => Issues filter { t1 =>
condition.repo condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos) .getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) } .map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) && .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed is (condition.state == "closed").bind) && (t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) && (t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && (t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && (t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && (t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(t1.pullRequest is true.bind, onlyPullRequest) && (t1.pullRequest === true.bind, onlyPullRequest) &&
(IssueLabels filter { t2 => (IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in (t2.labelId in
@@ -182,7 +184,8 @@ trait IssuesService {
} }
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) = assignedUserName: Option[String], milestoneId: Option[Int],
isPullRequest: Boolean = false)(implicit s: Session) =
// next id number // next id number
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
.firstOption.filter { id => .firstOption.filter { id =>
@@ -207,55 +210,57 @@ trait IssuesService {
.update (id) > 0 .update (id) > 0
} get } get
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
IssueLabels insert (IssueLabel(owner, repository, issueId, labelId)) IssueLabels insert IssueLabel(owner, repository, issueId, labelId)
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def createComment(owner: String, repository: String, loginUser: String, def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: String) = issueId: Int, content: String, action: String)(implicit s: Session): Int =
IssueComments.autoInc insert ( IssueComments.autoInc insert IssueComment(
owner, userName = owner,
repository, repositoryName = repository,
issueId, issueId = issueId,
action, action = action,
loginUser, commentedUserName = loginUser,
content, content = content,
currentDate, registeredDate = currentDate,
currentDate) updatedDate = currentDate)
def updateIssue(owner: String, repository: String, issueId: Int, def updateIssue(owner: String, repository: String, issueId: Int,
title: String, content: Option[String]) = title: String, content: Option[String])(implicit s: Session) =
Issues Issues
.filter (_.byPrimaryKey(owner, repository, issueId)) .filter (_.byPrimaryKey(owner, repository, issueId))
.map { t => .map { t =>
t.title ~ t.content.? ~ t.updatedDate (t.title, t.content.?, t.updatedDate)
} }
.update (title, content, currentDate) .update (title, content, currentDate)
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) = def updateAssignedUserName(owner: String, repository: String, issueId: Int,
assignedUserName: Option[String])(implicit s: Session) =
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) = def updateMilestoneId(owner: String, repository: String, issueId: Int,
milestoneId: Option[Int])(implicit s: Session) =
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
def updateComment(commentId: Int, content: String) = def updateComment(commentId: Int, content: String)(implicit s: Session) =
IssueComments IssueComments
.filter (_.byPrimaryKey(commentId)) .filter (_.byPrimaryKey(commentId))
.map { t => .map { t =>
t.content ~ t.updatedDate t.content -> t.updatedDate
} }
.update (content, currentDate) .update (content, currentDate)
def deleteComment(commentId: Int) = def deleteComment(commentId: Int)(implicit s: Session) =
IssueComments filter (_.byPrimaryKey(commentId)) delete IssueComments filter (_.byPrimaryKey(commentId)) delete
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) = def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) =
Issues Issues
.filter (_.byPrimaryKey(owner, repository, issueId)) .filter (_.byPrimaryKey(owner, repository, issueId))
.map { t => .map { t =>
t.closed ~ t.updatedDate t.closed -> t.updatedDate
} }
.update (closed, currentDate) .update (closed, currentDate)
@@ -267,8 +272,9 @@ trait IssuesService {
* @param query the keywords separated by whitespace. * @param query the keywords separated by whitespace.
* @return issues with comment count and matched content of issue or comment * @return issues with comment count and matched content of issue or comment
*/ */
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = { def searchIssuesByKeyword(owner: String, repository: String, query: String)
import scala.slick.driver.H2Driver.likeEncode (implicit s: Session): List[(Issue, Int, String)] = {
import slick.driver.JdbcDriver.likeEncode
val keywords = splitWords(query.toLowerCase) val keywords = splitWords(query.toLowerCase)
// Search Issue // Search Issue
@@ -304,7 +310,7 @@ trait IssuesService {
} }
issues.union(comments).sortBy { case (issue, commentId, _, _) => issues.union(comments).sortBy { case (issue, commentId, _, _) =>
issue.issueId ~ commentId issue.issueId -> commentId
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issue1.issueId == issue2.issueId issue1.issueId == issue2.issueId
}.map { _.head match { }.map { _.head match {
@@ -313,7 +319,7 @@ trait IssuesService {
}.toList }.toList
} }
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = { def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = {
extractCloseId(message).foreach { issueId => extractCloseId(message).foreach { issueId =>
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
createComment(owner, repository, userName, issue.issueId, "Close", "close") createComment(owner, repository, userName, issue.issueId, "Close", "close")

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
package service package service
import model._ import model.Profile._
import scala.slick.driver.H2Driver.simple._ import profile.simple._
import Database.threadLocalSession import model.{Repository, Account, Collaborator}
import util.JGitUtil import util.JGitUtil
trait RepositoryService { self: AccountService => trait RepositoryService { self: AccountService =>
@@ -20,7 +20,8 @@ trait RepositoryService { self: AccountService =>
*/ */
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None, originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = { parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
(implicit s: Session): Unit = {
Repositories insert Repositories insert
Repository( Repository(
userName = userName, userName = userName,
@@ -39,40 +40,50 @@ trait RepositoryService { self: AccountService =>
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = { def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String)
(implicit s: Session): Unit = {
getAccountByUserName(newUserName).foreach { account => getAccountByUserName(newUserName).foreach { account =>
(Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => (Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t => Repositories.filter { t =>
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind) (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t => Repositories.filter { t =>
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind) (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t => PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
deleteRepository(oldUserName, oldRepositoryName) deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
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)) :_*) PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
@@ -88,10 +99,10 @@ trait RepositoryService { self: AccountService =>
val updateActivities = Activities.filter { t => val updateActivities = Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId ~ t.message }.list }.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) => updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId is activityId.bind).map(_.message).update( Activities.filter(_.activityId === activityId.bind).map(_.message).update(
message message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#") .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
@@ -104,7 +115,7 @@ trait RepositoryService { self: AccountService =>
} }
} }
def deleteRepository(userName: String, repositoryName: String): Unit = { def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete Activities .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete Collaborators .filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
@@ -124,8 +135,8 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of repository owner * @param userName the user name of repository owner
* @return the list of repository names * @return the list of repository names
*/ */
def getRepositoryNamesOfUser(userName: String): List[String] = def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list Repositories filter(_.userName === userName.bind) map (_.repositoryName) list
/** /**
* Returns the specified repository information. * Returns the specified repository information.
@@ -135,11 +146,11 @@ trait RepositoryService { self: AccountService =>
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @return the repository information * @return the repository information
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count // for getting issue count and pull request count
val issues = Query(Issues).filter { t => val issues = Issues.filter { t =>
t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind) t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind)
}.map(_.pullRequest).list }.map(_.pullRequest).list
new RepositoryInfo( new RepositoryInfo(
@@ -155,13 +166,24 @@ trait RepositoryService { self: AccountService =>
} }
} }
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = { def getAllRepositories()(implicit s: Session): List[(String, String)] = {
Query(Repositories).filter { t1 => Repositories.sortBy(_.lastActivityDate desc).map{ t =>
(t1.userName is userName.bind) || (t.userName, t.repositoryName)
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists) }.list
}
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
@@ -178,24 +200,32 @@ trait RepositoryService { self: AccountService =>
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = { def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories) case Some(x) if(x.isAdmin) => Repositories
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) || Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists) (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists)
} }
// for Guests // for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind) case None => Repositories filter(_.isPrivate === false.bind)
}).filter { t => }).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
@@ -205,7 +235,7 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getRepositoryManagers(userName: String): Seq[String] = private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){ if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else { } else {
@@ -215,16 +245,16 @@ trait RepositoryService { self: AccountService =>
/** /**
* Updates the last activity date of the repository. * Updates the last activity date of the repository.
*/ */
def updateLastActivityDate(userName: String, repositoryName: String): Unit = def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate) Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
/** /**
* Save repository options. * Save repository options.
*/ */
def saveRepositoryOptions(userName: String, repositoryName: String, def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], defaultBranch: String, isPrivate: Boolean): Unit = description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)) Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => r.description.? ~ r.defaultBranch ~ r.isPrivate ~ r.updatedDate } .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
.update (description, defaultBranch, isPrivate, currentDate) .update (description, defaultBranch, isPrivate, currentDate)
/** /**
@@ -234,8 +264,8 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators insert(Collaborator(userName, repositoryName, collaboratorName)) Collaborators insert Collaborator(userName, repositoryName, collaboratorName)
/** /**
* Remove collaborator from the repository. * Remove collaborator from the repository.
@@ -244,7 +274,7 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/** /**
@@ -253,7 +283,7 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param repositoryName the repository name * @param repositoryName the repository name
*/ */
def removeCollaborators(userName: String, repositoryName: String): Unit = def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/** /**
@@ -263,10 +293,10 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @return the list of collaborators name * @return the list of collaborators name
*/ */
def getCollaborators(userName: String, repositoryName: String): List[String] = def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] =
Query(Collaborators).filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account]): Boolean = { def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match { loginAccount match {
case Some(a) if(a.isAdmin) => true case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true case Some(a) if(a.userName == owner) => true
@@ -275,17 +305,17 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getForkedCount(userName: String, repositoryName: String): Int = private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
Query(Repositories.filter { t => Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
}.length).first }.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] = def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
Query(Repositories).filter { t => Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
} }
.sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
} }

View File

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

View File

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

View File

@@ -37,13 +37,16 @@ trait SystemSettingsService {
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
props.setProperty(LdapBaseDN, ldap.baseDN) props.setProperty(LdapBaseDN, ldap.baseDN)
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x.toString))
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
} }
} }
props.store(new java.io.FileOutputStream(GitBucketConf), null) using(new java.io.FileOutputStream(GitBucketConf)){ out =>
props.store(out, null)
}
} }
} }
@@ -51,7 +54,9 @@ trait SystemSettingsService {
def loadSystemSettings(): SystemSettings = { def loadSystemSettings(): SystemSettings = {
defining(new java.util.Properties()){ props => defining(new java.util.Properties()){ props =>
if(GitBucketConf.exists){ if(GitBucketConf.exists){
props.load(new java.io.FileInputStream(GitBucketConf)) using(new java.io.FileInputStream(GitBucketConf)){ in =>
props.load(in)
}
} }
SystemSettings( SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
@@ -81,8 +86,9 @@ trait SystemSettingsService {
getOptionValue(props, LdapBindPassword, None), getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""), getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""), getValue(props, LdapUserNameAttribute, ""),
getOptionValue(props, LdapAdditionalFilterCondition, None),
getOptionValue(props, LdapFullNameAttribute, None), getOptionValue(props, LdapFullNameAttribute, None),
getValue(props, LdapMailAddressAttribute, ""), getOptionValue(props, LdapMailAddressAttribute, None),
getOptionValue[Boolean](props, LdapTls, None), getOptionValue[Boolean](props, LdapTls, None),
getOptionValue(props, LdapKeystore, None))) getOptionValue(props, LdapKeystore, None)))
} else { } else {
@@ -111,7 +117,7 @@ object SystemSettingsService {
defining(request.getRequestURL.toString){ url => defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
} }
}.replaceFirst("/$", "") }.stripSuffix("/")
} }
case class Ldap( case class Ldap(
@@ -121,8 +127,9 @@ object SystemSettingsService {
bindPassword: Option[String], bindPassword: Option[String],
baseDN: String, baseDN: String,
userNameAttribute: String, userNameAttribute: String,
additionalFilterCondition: Option[String],
fullNameAttribute: Option[String], fullNameAttribute: Option[String],
mailAttribute: String, mailAttribute: Option[String],
tls: Option[Boolean], tls: Option[Boolean],
keystore: Option[String]) keystore: Option[String])
@@ -159,6 +166,7 @@ object SystemSettingsService {
private val LdapBindPassword = "ldap.bind_password" private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN" private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute" private val LdapUserNameAttribute = "ldap.username_attribute"
private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
private val LdapFullNameAttribute = "ldap.fullname_attribute" private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute" private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls" private val LdapTls = "ldap.tls"

View File

@@ -1,9 +1,8 @@
package service package service
import scala.slick.driver.H2Driver.simple._ import model.Profile._
import Database.threadLocalSession import profile.simple._
import model.{WebHook, Account}
import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
import util.JGitUtil import util.JGitUtil
@@ -12,7 +11,6 @@ import util.JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.protocol.HTTP
import org.apache.http.NameValuePair import org.apache.http.NameValuePair
trait WebHookService { trait WebHookService {
@@ -20,14 +18,14 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService]) private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String): List[WebHook] = def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] =
Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list
def addWebHookURL(owner: String, repository: String, url :String): Unit = def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.insert(WebHook(owner, repository, url)) WebHooks insert WebHook(owner, repository, url)
def deleteWebHookURL(owner: String, repository: String, url :String): Unit = def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = { def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
import org.json4s._ import org.json4s._
@@ -46,7 +44,7 @@ trait WebHookService {
val httpClient = HttpClientBuilder.create.build val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl => webHookURLs.foreach { webHookUrl =>
val f = future { val f = Future {
logger.debug(s"start web hook invocation for ${webHookUrl}") logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHookUrl.url)
@@ -87,23 +85,23 @@ object WebHookService {
refName, refName,
commits.map { commit => commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false) val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id
WebHookCommit( WebHookCommit(
id = commit.id, id = commit.id,
message = commit.fullMessage, message = commit.fullMessage,
timestamp = commit.time.toString, timestamp = commit.commitTime.toString,
url = commitUrl, url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath }, added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD && modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser( author = WebHookUser(
name = commit.committer, name = commit.committerName,
email = commit.mailAddress email = commit.committerEmailAddress
) )
) )
}.toList, },
WebHookRepository( WebHookRepository(
name = repositoryInfo.name, name = repositoryInfo.name,
url = repositoryInfo.httpUrl, url = repositoryInfo.httpUrl,

View File

@@ -2,22 +2,18 @@ package service
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils
import util._ import util._
import _root_.util.ControlUtil._ import _root_.util.ControlUtil._
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser} import org.eclipse.jgit.treewalk.CanonicalTreeParser
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry} import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter} import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._ import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import scala.Some
import service.RepositoryService.RepositoryInfo import service.RepositoryService.RepositoryInfo
object WikiService { object WikiService {
/** /**
@@ -68,7 +64,7 @@ trait WikiService {
if(!JGitUtil.isEmpty(git)){ if(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), 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 } else None
} }
@@ -97,7 +93,7 @@ trait WikiService {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
JGitUtil.getFileList(git, "master", ".") JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md")) .filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", "")) .map(_.name.stripSuffix(".md"))
.sortBy(x => x) .sortBy(x => x)
} }
} }
@@ -143,7 +139,7 @@ trait WikiService {
val revertInfo = (p.getFiles.asScala.map { fh => val revertInfo = (p.getFiles.asScala.map { fh =>
fh.getChangeType match { fh.getChangeType match {
case DiffEntry.ChangeType.MODIFY => { case DiffEntry.ChangeType.MODIFY => {
val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("") val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("")
val applied = PatchUtil.apply(source, patch, fh) val applied = PatchUtil.apply(source, patch, fh)
if(applied != null){ if(applied != null){
Seq(RevertInfo("ADD", fh.getNewPath, applied)) Seq(RevertInfo("ADD", fh.getNewPath, applied))

View File

@@ -10,6 +10,7 @@ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory import util.Directory
import plugin.PluginUpdateJob
object AutoUpdate { object AutoUpdate {
@@ -51,6 +52,8 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){ new Version(2, 0){
override def update(conn: Connection): Unit = { override def update(conn: Connection): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType} import eu.medsea.mimeutil.{MimeUtil2, MimeType}
@@ -143,8 +146,14 @@ object AutoUpdate {
* Update database schema automatically in the context initializing. * Update database schema automatically in the context initializing.
*/ */
class AutoUpdateListener extends ServletContextListener { class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
import AutoUpdate._ import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler
override def contextInitialized(event: ServletContextEvent): Unit = { override def contextInitialized(event: ServletContextEvent): Unit = {
val datadir = event.getServletContext.getInitParameter("gitbucket.home") val datadir = event.getServletContext.getInitParameter("gitbucket.home")
@@ -178,10 +187,19 @@ class AutoUpdateListener extends ServletContextListener {
} }
} }
logger.debug("End schema update") logger.debug("End schema update")
logger.debug("Starting plugin system...")
plugin.PluginSystem.init()
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
} }
def contextDestroyed(sce: ServletContextEvent): Unit = { def contextDestroyed(sce: ServletContextEvent): Unit = {
// Nothing to do. scheduler.shutdown()
} }
private def getConnection(servletContext: ServletContext): Connection = private def getConnection(servletContext: ServletContext): Connection =

View File

@@ -3,7 +3,7 @@ package servlet
import javax.servlet._ import javax.servlet._
import javax.servlet.http._ import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService} import service.{SystemSettingsService, AccountService, RepositoryService}
import model.Account import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
@@ -21,7 +21,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
def destroy(): Unit = {} def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
val request = req.asInstanceOf[HttpServletRequest] implicit val request = req.asInstanceOf[HttpServletRequest]
val response = res.asInstanceOf[HttpServletResponse] val response = res.asInstanceOf[HttpServletResponse]
val wrappedResponse = new HttpServletResponseWrapper(response){ val wrappedResponse = new HttpServletResponseWrapper(response){
@@ -65,7 +65,8 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
} }
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Option[Account] = private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
(implicit session: Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match { authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case _ => None case _ => None

View File

@@ -17,6 +17,7 @@ import WebHookService._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import service.IssuesService.IssueSearchCondition import service.IssuesService.IssueSearchCondition
import model.Session
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -76,15 +77,17 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "") val repository = paths(2).stripSuffix(".git")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
val hook = new CommitLogHook(owner, repository, pusher, baseUrl(request)) defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPreReceiveHook(hook) receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook) receivePack.setPostReceiveHook(hook)
} }
}
receivePack receivePack
} }
} }
@@ -92,7 +95,8 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook with PreReceiveHook class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -114,6 +118,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try { try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command => commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
@@ -133,10 +138,16 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
val newCommits = commits.flatMap { commit => val newCommits = commits.flatMap { commit =>
if (!existIds.contains(commit.id)) { if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) { if (issueCount > 0) {
pushedIds.add(commit.id)
createIssueComment(commit) createIssueComment(commit)
// close issues
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
}
} }
Some(commit) Some(commit)
} else None } else None
@@ -168,17 +179,6 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
} }
} }
// close issues
if(issueCount > 0) {
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if (refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE) {
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach {
commit =>
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
}
}
}
// call web hook // call web hook
getWebHookURLs(owner, repository) match { getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -205,7 +205,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
private def createIssueComment(commit: CommitInfo) = { private def createIssueComment(commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){ if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.mailAddress).foreach { account => getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
} }
} }

View File

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

View File

@@ -3,6 +3,7 @@ package servlet
import javax.servlet._ import javax.servlet._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import util.Keys
/** /**
* Controls the transaction with the open session in view pattern. * Controls the transaction with the open session in view pattern.
@@ -20,8 +21,9 @@ class TransactionFilter extends Filter {
// assets don't need transaction // assets don't need transaction
chain.doFilter(req, res) chain.doFilter(req, res)
} else { } else {
Database(req.getServletContext) withTransaction { Database(req.getServletContext) withTransaction { session =>
logger.debug("begin transaction") logger.debug("begin transaction")
req.setAttribute(Keys.Request.DBSession, session)
chain.doFilter(req, res) chain.doFilter(req, res)
logger.debug("end transaction") logger.debug("end transaction")
} }
@@ -31,8 +33,13 @@ class TransactionFilter extends Filter {
} }
object Database { object Database {
def apply(context: ServletContext): scala.slick.session.Database =
scala.slick.session.Database.forURL(context.getInitParameter("db.url"), def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database =
slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"), context.getInitParameter("db.user"),
context.getInitParameter("db.password")) context.getInitParameter("db.password"))
def getSession(req: ServletRequest): slick.jdbc.JdbcBackend#Session =
req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
} }

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,10 @@ object Directory {
val DatabaseHome = s"${GitBucketHome}/data" val DatabaseHome = s"${GitBucketHome}/data"
val PluginHome = s"${GitBucketHome}/plugins"
val TemporaryHome = s"${GitBucketHome}/tmp"
/** /**
* Substance directory of the repository. * Substance directory of the repository.
*/ */
@@ -55,13 +59,18 @@ object Directory {
* Root of temporary directories for the upload file. * Root of temporary directories for the upload file.
*/ */
def getTemporaryDir(sessionId: String): File = def getTemporaryDir(sessionId: String): File =
new File(s"${GitBucketHome}/tmp/_upload/${sessionId}") new File(s"${TemporaryHome}/_upload/${sessionId}")
/** /**
* Root of temporary directories for the specified repository. * Root of temporary directories for the specified repository.
*/ */
def getTemporaryDir(owner: String, repository: String): File = def getTemporaryDir(owner: String, repository: String): File =
new File(s"${GitBucketHome}/tmp/${owner}/${repository}") new File(s"${TemporaryHome}/${owner}/${repository}")
/**
* Root of plugin cache directory. Plugin repositories are cloned into this directory.
*/
def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
/** /**
* Temporary directory which is used to create an archive to download repository contents. * Temporary directory which is used to create an archive to download repository contents.

View File

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

View File

@@ -35,7 +35,11 @@ object JGitUtil {
* @param branchList the list of branch names * @param branchList the list of branch names
* @param tags the list of tags * @param tags the list of tags
*/ */
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]) case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
def this(owner: String, name: String, baseUrl: String) = {
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
}
}
/** /**
* The file data for the file list of the repository viewer. * The file data for the file list of the repository viewer.
@@ -43,38 +47,45 @@ object JGitUtil {
* @param id the object id * @param id the object id
* @param isDirectory whether is it directory * @param isDirectory whether is it directory
* @param name the file (or directory) name * @param name the file (or directory) name
* @param time the last modified time
* @param message the last commit message * @param message the last commit message
* @param commitId the last commit id * @param commitId the last commit id
* @param committer the last committer name * @param time the last modified time
* @param author the last committer name
* @param mailAddress the committer's mail address * @param mailAddress the committer's mail address
* @param linkUrl the url of submodule * @param linkUrl the url of submodule
*/ */
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String,
committer: String, mailAddress: String, linkUrl: Option[String]) time: Date, author: String, mailAddress: String, linkUrl: Option[String])
/** /**
* The commit data. * The commit data.
* *
* @param id the commit id * @param id the commit id
* @param time the commit time
* @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message * @param shortMessage the short message
* @param fullMessage the full message * @param fullMessage the full message
* @param parents the list of parent commit id * @param parents the list of parent commit id
* @param authorTime the author time
* @param authorName the author name
* @param authorEmailAddress the mail address of the author
* @param commitTime the commit time
* @param committerName the committer name
* @param committerEmailAddress the mail address of the committer
*/ */
case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String, case class CommitInfo(id: String, shortMessage: String, fullMessage: String, parents: List[String],
shortMessage: String, fullMessage: String, parents: List[String]){ authorTime: Date, authorName: String, authorEmailAddress: String,
commitTime: Date, committerName: String, committerEmailAddress: String){
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
rev.getName, rev.getName,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress,
rev.getShortMessage, rev.getShortMessage,
rev.getFullMessage, rev.getFullMessage,
rev.getParents().map(_.name).toList) rev.getParents().map(_.name).toList,
rev.getAuthorIdent.getWhen,
rev.getAuthorIdent.getName,
rev.getAuthorIdent.getEmailAddress,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress)
val summary = getSummaryMessage(fullMessage, shortMessage) val summary = getSummaryMessage(fullMessage, shortMessage)
@@ -83,6 +94,8 @@ object JGitUtil {
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} else None } else None
} }
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
} }
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -94,7 +107,12 @@ object JGitUtil {
* @param content the string content * @param content the string content
* @param charset the character encoding * @param charset the character encoding
*/ */
case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]) case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]){
/**
* the line separator of this content ("LF" or "CRLF")
*/
val lineSeparator: String = if(content.exists(_.indexOf("\r\n") >= 0)) "CRLF" else "LF"
}
/** /**
* The tag data. * The tag data.
@@ -146,12 +164,12 @@ object JGitUtil {
commitCount, commitCount,
// branches // branches
git.branchList.call.asScala.map { ref => git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "") ref.getName.stripPrefix("refs/heads/")
}.toList, }.toList,
// tags // tags
git.tagList.call.asScala.map { ref => git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId) val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName) TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList }.toList
) )
} catch { } catch {
@@ -190,7 +208,7 @@ object JGitUtil {
val targetPath = walker.getPathString val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){ if((path + "/").startsWith(targetPath)){
true true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){ } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){
stopRecursive = true stopRecursive = true
treeWalk.setRecursive(false) treeWalk.setRecursive(false)
true true
@@ -222,11 +240,11 @@ object JGitUtil {
objectId, objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name, name,
commit.getCommitterIdent.getWhen,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage), getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName, commit.getName,
commit.getCommitterIdent.getName, commit.getAuthorIdent.getWhen,
commit.getCommitterIdent.getEmailAddress, commit.getAuthorIdent.getName,
commit.getAuthorIdent.getEmailAddress,
linkUrl) linkUrl)
} }
}.sortWith { (file1, file2) => }.sortWith { (file1, file2) =>

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import app.Context import app.Context
import model.Session
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database import servlet.Database
import SystemSettingsService.Smtp import SystemSettingsService.Smtp
@@ -15,7 +16,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit (msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) = protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
( (
// individual repository's owner // individual repository's owner
issue.userName :: issue.userName ::
@@ -27,7 +28,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
) )
.distinct .distinct
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded .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)) )
} }
@@ -68,9 +69,8 @@ class Mailer(private val smtp: Smtp) extends Notifier {
(msg: String => String)(implicit context: Context) = { (msg: String => String)(implicit context: Context) = {
val database = Database(context.request.getServletContext) val database = Database(context.request.getServletContext)
val f = future { val f = Future {
// TODO Can we use the Database Session in other than Transaction Filter? database withSession { implicit session =>
database withSession {
getIssue(r.owner, r.name, issueId.toString) foreach { issue => getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining( defining(
s"[${r.name}] ${issue.title} (#${issueId})" -> s"[${r.name}] ${issue.title} (#${issueId})" ->

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
(text, text) (text, text)
} }
val url = repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){ if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label) new Rendering(url, label)
@@ -105,7 +105,7 @@ class GitBucketHtmlSerializer(
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){ if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){
url url
} else { } else {
repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
} }
} }

View File

@@ -74,7 +74,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* This method looks up Gravatar if avatar icon has not been configured in user settings. * This method looks up Gravatar if avatar icon has not been configured in user settings.
*/ */
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html = def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
getAvatarImageHtml(commit.committer, size, commit.mailAddress) getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress)
/** /**
* Converts commit id, issue id and username to the link. * Converts commit id, issue id and username to the link.
@@ -196,7 +196,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
case x if(x.endsWith(".sql")) => "sql" case x if(x.endsWith(".sql")) => "sql"
case x if(x.endsWith(".tcl")) => "tcl" case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".vbs")) => "vbscript" case x if(x.endsWith(".vbs")) => "vbscript"
case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".yml")) => "yaml" case x if(x.endsWith(".yml")) => "yaml"
case _ => "plain_text" case _ => "plain_text"
} }

View File

@@ -1,6 +1,7 @@
@(account: model.Account, info: Option[Any])(implicit context: app.Context) @(account: model.Account, info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import util.LDAPUtil
@html.main("Edit your profile"){ @html.main("Edit your profile"){
<div class="container"> <div class="container">
<div class="row-fluid"> <div class="row-fluid">
@@ -9,6 +10,7 @@
</div> </div>
<div class="span9"> <div class="span9">
@helper.html.information(info) @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"> <form action="@url(account.userName)/_edit" method="POST" validate="true">
<div class="box"> <div class="box">
<div class="box-header">Profile</div> <div class="box-header">Profile</div>
@@ -31,7 +33,7 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="mailAddress" class="strong">Mail Address:</label> <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> <span id="error-mailAddress" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>
@@ -52,7 +54,7 @@
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a> <a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div> </div>
<input type="submit" class="btn btn-success" value="Save"/> <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> </div>
</div> </div>

View File

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

View File

@@ -8,7 +8,7 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="span4"> <div class="span4">
<div class="block"> <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-fullname">@account.fullName</div>
<div class="account-username">@account.userName</div> <div class="account-username">@account.userName</div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -133,6 +133,13 @@
<span id="error-ldap_userNameAttribute" class="error"></span> <span id="error-ldap_userNameAttribute" class="error"></span>
</div> </div>
</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"> <div class="control-group">
<label class="control-label" for="ldapFullNameAttribute">Full name attribute</label> <label class="control-label" for="ldapFullNameAttribute">Full name attribute</label>
<div class="controls"> <div class="controls">

View File

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

View File

@@ -22,7 +22,7 @@
case "fork" => simpleActivity(activity, "activity-fork.png") case "fork" => simpleActivity(activity, "activity-fork.png")
case "push" => customActivity(activity, "activity-commit.png"){ case "push" => customActivity(activity, "activity-commit.png"){
<div class="small activity-message"> <div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => {activity.additionalInfo.get.split("\n").reverse.filter(_ matches "[0-9a-z]{40}:.*").take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){ if(i == 3){
<div>...</div> <div>...</div>
} else { } else {

View File

@@ -24,6 +24,7 @@
}); });
var title = $('#@id').attr('title'); var title = $('#@id').attr('title');
$('#@id').removeAttr('title') $('#@id').removeAttr('title')
clip.htmlBridge = "#global-zeroclipboard-html-bridge";
clip.on('complete', function(client, args) { clip.on('complete', function(client, args) {
$(clip.htmlBridge).attr('title', 'copied!').tooltip('fixTitle').tooltip('show'); $(clip.htmlBridge).attr('title', 'copied!').tooltip('fixTitle').tooltip('show');
$(clip.htmlBridge).attr('title', title).tooltip('fixTitle'); $(clip.htmlBridge).attr('title', title).tooltip('fixTitle');

View File

@@ -78,9 +78,9 @@
</tr> </tr>
</table> </table>
} }
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script> <script>
$(function(){ $(function(){
@if(showIndex){ @if(showIndex){

View File

@@ -25,8 +25,8 @@
</div> </div>
</div> </div>
</div> </div>
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/> <link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<script src="@assets/google-code-prettify/prettify.js"></script> <script src="@assets/vendors/google-code-prettify/prettify.js"></script>
<script> <script>
$(function(){ $(function(){
@if(elastic){ @if(elastic){

View File

@@ -1,4 +1,5 @@
@(issue: model.Issue, @(issue: model.Issue,
reopenable: Boolean,
hasWritePermission: Boolean, hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@@ -14,7 +15,7 @@
<div class="pull-right"> <div class="pull-right">
<input type="hidden" name="issueId" value="@issue.issueId"/> <input type="hidden" name="issueId" value="@issue.issueId"/>
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/> <input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
@if((!issue.isPullRequest || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){ @if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/> <input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
} }
</div> </div>

View File

@@ -96,7 +96,7 @@ $(function(){
}); });
return false; return false;
}); });
$('i.icon-remove-circle').click(function(){ $('.issue-comment-box i.icon-remove-circle').click(function(){
if(confirm('Are you sure you want to delete this?')) { if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id'); var id = $(this).closest('a').data('comment-id');
$.post('@url(repository)/issue_comments/delete/' + id, $.post('@url(repository)/issue_comments/delete/' + id,

View File

@@ -99,7 +99,7 @@ $(function(){
$('#label-assigned').html($('<span>') $('#label-assigned').html($('<span>')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName)) .append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' will be assigned')); .append(' will be assigned'));
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok'); $('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
} }
$('input[name=assignedUserName]').val(userName); $('input[name=assignedUserName]').val(userName);
}); });

View File

@@ -11,10 +11,13 @@
<script> <script>
$(function(){ $(function(){
var callback = function(data){ var callback = function(data){
$('#update-comment-@commentId, #cancel-comment-@commentId').removeAttr('disabled');
$('#commentContent-@commentId').empty().html(data.content); $('#commentContent-@commentId').empty().html(data.content);
prettyPrint();
}; };
$('#update-comment-@commentId').click(function(){ $('#update-comment-@commentId').click(function(){
$('#update-comment-@commentId, #cancel-comment-@commentId').attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issue_comments/edit/@commentId', url: '@path/@owner/@repository/issue_comments/edit/@commentId',
type: 'POST', type: 'POST',
@@ -25,11 +28,13 @@ $(function(){
}).done( }).done(
callback callback
).fail(function(req) { ).fail(function(req) {
$('#update-comment-@commentId, #cancel-comment-@commentId').removeAttr('disabled');
$('#error-edit-content-@commentId').text($.parseJSON(req.responseText).content); $('#error-edit-content-@commentId').text($.parseJSON(req.responseText).content);
}); });
}); });
$('#cancel-comment-@commentId').click(function(){ $('#cancel-comment-@commentId').click(function(){
$('#update-comment-@commentId, #cancel-comment-@commentId').attr('disabled', 'disabled');
$.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback); $.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback);
return false; return false;
}); });

View File

@@ -14,11 +14,13 @@ $(function(){
$('#edit-content').elastic(); $('#edit-content').elastic();
var callback = function(data){ var callback = function(data){
$('#update, #cancel').removeAttr('disabled');
$('#issueTitle').empty().text(data.title); $('#issueTitle').empty().text(data.title);
$('#issueContent').empty().html(data.content); $('#issueContent').empty().html(data.content);
}; };
$('#update').click(function(){ $('#update').click(function(){
$('#update, #cancel').attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issues/edit/@issueId', url: '@path/@owner/@repository/issues/edit/@issueId',
type: 'POST', type: 'POST',
@@ -29,11 +31,13 @@ $(function(){
}).done( }).done(
callback callback
).fail(function(req) { ).fail(function(req) {
$('#update, #cancel').removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title); $('#error-edit-title').text($.parseJSON(req.responseText).title);
}); });
}); });
$('#cancel').click(function(){ $('#cancel').click(function(){
$('#update, #cancel').attr('disabled', 'disabled');
$.get('@path/@owner/@repository/issues/_data/@issueId', callback); $.get('@path/@owner/@repository/issues/_data/@issueId', callback);
return false; return false;
}); });

View File

@@ -19,7 +19,7 @@
<div class="span10"> <div class="span10">
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository) @issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@commentlist(issue, comments, hasWritePermission, repository) @commentlist(issue, comments, hasWritePermission, repository)
@commentform(issue, hasWritePermission, repository) @commentform(issue, true, hasWritePermission, repository)
</div> </div>
<div class="span2"> <div class="span2">
@if(issue.closed) { @if(issue.closed) {

View File

@@ -53,7 +53,7 @@
</div> </div>
@if(hasWritePermission){ @if(hasWritePermission){
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li> <li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) => @milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li> <li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title"> <a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@@ -116,7 +116,7 @@ $(function(){
.append($this.find('img.avatar').clone(false)).append(' ') .append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName)) .append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned'); .append(' is assigned');
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok'); $('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
} }
}); });
}); });

View File

@@ -9,26 +9,26 @@
<link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" /> <link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Le styles --> <!-- Le styles -->
<link href="@assets/bootstrap/css/bootstrap.css" rel="stylesheet"> <link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
<link href="@assets/bootstrap/css/bootstrap-responsive.css" rel="stylesheet"> <link href="@assets/vendors/bootstrap/css/bootstrap-responsive.css" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="@assets/bootstrap/js/html5shiv.js"></script> <script src="@assets/vendors/bootstrap/js/html5shiv.js"></script>
<![endif]--> <![endif]-->
<link href="@assets/datepicker/css/datepicker.css" rel="stylesheet"> <link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
<link href="@assets/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet"> <link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/> <link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/common/css/gitbucket.css" rel="stylesheet"> <link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/common/js/jquery-1.9.1.js"></script> <script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
<script src="@assets/common/js/dropzone.js"></script> <script src="@assets/vendors/dropzone/dropzone.js"></script>
<script src="@assets/common/js/validation.js"></script> <script src="@assets/common/js/validation.js"></script>
<script src="@assets/common/js/gitbucket.js"></script> <script src="@assets/common/js/gitbucket.js"></script>
<script src="@assets/bootstrap/js/bootstrap.js"></script> <script src="@assets/vendors/bootstrap/js/bootstrap.js"></script>
<script src="@assets/datepicker/js/bootstrap-datepicker.js"></script> <script src="@assets/vendors/datepicker/js/bootstrap-datepicker.js"></script>
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script> <script src="@assets/vendors/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/google-code-prettify/prettify.js"></script> <script src="@assets/vendors/google-code-prettify/prettify.js"></script>
<script src="@assets/zclip/ZeroClipboard.min.js"></script> <script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
<script src="@assets/elastic/jquery.elastic.source.js"></script> <script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
</head> </head>
<body> <body>
<form id="search" action="@path/search" method="POST"> <form id="search" action="@path/search" method="POST">
@@ -60,18 +60,28 @@
<li><a href="@path/groups/new">New group</a></li> <li><a href="@path/groups/new">New group</a></li>
</ul> </ul>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a> <a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
@if(loginAccount.get.isAdmin){ @if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a> <a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
} }
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a> <a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else { } else {
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a> <a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
} }
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</div> </div>
</div> </div>
</form>
</div> </div>
</form>
@body @body
<script> <script>
$(function(){ $(function(){

View File

@@ -23,6 +23,13 @@
</li> </li>
} }
@sidemenuPlugin(path: String, name: String, label: String, icon: String) = {
<li @if(active == name){class="active"}>
<div class="@if(active == name){margin} else {gradient} pull-left"></div>
<a href="@url(repository)@path"><img src="@icon"/>@if(expand){ @label}</a>
</li>
}
<div class="container"> <div class="container">
@if(repository.commitCount > 0){ @if(repository.commitCount > 0){
<div class="pull-right"> <div class="pull-right">
@@ -54,6 +61,11 @@
@sidemenu("/issues", "issues", "Issues", repository.issueCount) @sidemenu("/issues", "issues", "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount) @sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "Wiki") @sidemenu("/wiki" , "wiki" , "Wiki")
@plugin.PluginSystem.repositoryMenus.map { menu =>
@if(menu.condition(context)){
@sidemenuPlugin(menu.url, menu.label, menu.label, menu.icon)
}
}
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){ @if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@sidemenu("/settings", "settings", "Settings") @sidemenu("/settings", "settings", "Settings")
} }
@@ -75,6 +87,9 @@
<div style="margin-top: 10px;"> <div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-small" style="width: 147px;"><i class="icon-download-alt"></i>Download ZIP</a> <a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-small" style="width: 147px;"><i class="icon-download-alt"></i>Download ZIP</a>
</div> </div>
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.tar.gz" class="btn btn-small" style="width: 147px;"><i class="icon-download-alt"></i>Download TAR.GZ</a>
</div>
} }
} }
</div> </div>

View File

@@ -6,13 +6,13 @@
<table class="table table-file-list" style="border: 1px solid silver;"> <table class="table table-file-list" style="border: 1px solid silver;">
@commits.map { day => @commits.map { day =>
<tr> <tr>
<th colspan="3" class="box-header" style="font-weight: normal;">@date(day.head.time)</th> <th colspan="3" class="box-header" style="font-weight: normal;">@date(day.head.commitTime)</th>
</tr> </tr>
@day.map { commit => @day.map { commit =>
<tr> <tr>
<td style="width: 20%;"> <td style="width: 20%;">
@avatar(commit, 20) @avatar(commit, 20)
@user(commit.committer, commit.mailAddress, "username") @user(commit.authorName, commit.authorEmailAddress, "username")
</td> </td>
<td>@commit.shortMessage</td> <td>@commit.shortMessage</td>
<td style="width: 10%; text-align: right;"> <td style="width: 10%; text-align: right;">

View File

@@ -0,0 +1,90 @@
@(issue: model.Issue,
pullreq: model.PullRequest,
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="row-fluid">
<div class="span10">
@issues.html.issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
@defining(comments.exists(_.action == "merge")){ merged =>
@if(hasWritePermission && !issue.closed){
<div class="box issue-comment-box" style="background-color: #d8f5cd;">
<div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;">
<div id="merge-pull-request">
<div class="check-conflict" style="display: none;">
<img src="@assets/common/images/indicator.gif"/> Checking...
</div>
</div>
<div id="confirm-merge-form" style="display: none;">
<form method="POST" action="@url(repository)/pull/@issue.issueId/merge">
<div class="strong">
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div>
<span id="error-message" class="error"></span>
<textarea name="message" style="width: 635px; height: 80px;">@issue.title</textarea>
<div>
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
<input type="submit" class="btn btn-success" value="Confirm merge"/>
</div>
</form>
</div>
</div>
</div>
}
@if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged &&
pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
<div class="box issue-comment-box" style="background-color: #d0eeff;">
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
<a href="@url(repository)/pull/@issue.issueId/delete/@encodeRefName(pullreq.requestBranch)" class="btn btn-info pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
<div>
<span class="strong">Pull request successfully merged and closed</span>
</div>
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
</div>
</div>
}
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
</div>
<div class="span2">
@if(issue.closed) {
@if(merged){
<span class="label label-info issue-status">Merged</span>
} else {
<span class="label label-important issue-status">Closed</span>
}
} else {
<span class="label label-success issue-status">Open</span>
}
<div class="small" style="text-align: center;">
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
</div>
}
<hr/>
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
</div>
</div>
<script>
$(function(){
$('#cancel-merge-pull-request').click(function(){
$('#confirm-merge-form').hide();
$('#merge-pull-request').show();
});
@if(hasWritePermission){
$('.check-conflict').show();
$.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
});
}
});
</script>

View File

@@ -1,88 +0,0 @@
@(issue: model.Issue,
pullreq: model.PullRequest,
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="row-fluid">
<div class="span10">
@issues.html.issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
@if(hasWritePermission && !issue.closed){
<div class="box issue-comment-box" style="background-color: #d8f5cd;">
<div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;">
<div id="merge-pull-request">
<div class="check-conflict" style="display: none;">
<img src="@assets/common/images/indicator.gif"/> Checking...
</div>
</div>
<div id="confirm-merge-form" style="display: none;">
<form method="POST" action="@url(repository)/pull/@issue.issueId/merge">
<div class="strong">
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div>
<span id="error-message" class="error"></span>
<textarea name="message" style="width: 635px; height: 80px;">@issue.title</textarea>
<div>
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
<input type="submit" class="btn btn-success" value="Confirm merge"/>
</div>
</form>
</div>
</div>
</div>
}
@if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName &&
pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
<div class="box issue-comment-box" style="background-color: #d0eeff;">
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
<a href="@url(repository)/pull/@issue.issueId/delete/@pullreq.requestBranch" class="btn btn-info pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
<div>
<span class="strong">Pull request successfully merged and closed</span>
</div>
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
</div>
</div>
}
@issues.html.commentform(issue, hasWritePermission, repository)
</div>
<div class="span2">
@if(issue.closed) {
@if(comments.exists(_.action == "merge")){
<span class="label label-info issue-status">Merged</span>
} else {
<span class="label label-important issue-status">Closed</span>
}
} else {
<span class="label label-success issue-status">Open</span>
}
<div class="small" style="text-align: center;">
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
</div>
<hr/>
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
</div>
</div>
<script>
$(function(){
$('#cancel-merge-pull-request').click(function(){
$('#confirm-merge-form').hide();
$('#merge-pull-request').show();
});
@if(hasWritePermission){
$('.check-conflict').show();
$.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
});
}
});
</script>

View File

@@ -12,7 +12,7 @@
@if(hasWritePermission){ @if(hasWritePermission){
<div class="pull-right"> <div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL) @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL)
<a href="@url(repository)/compare" class="btn btn-success">New pull request</a> <a href="@url(repository)/compare" class="btn btn-small btn-success">New pull request</a>
</div> </div>
} }
} }

View File

@@ -42,7 +42,7 @@
<p> <p>
<span class="strong">Step 1:</span> Check out a new branch to test the changes — run this from your project directory <span class="strong">Step 1:</span> Check out a new branch to test the changes — run this from your project directory
</p> </p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.requestBranch}"){ command => @defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){ @helper.html.copy("merge-command-copy-1", command){
<pre style="width: 500px; float: left;">@command</pre> <pre style="width: 500px; float: left;">@command</pre>
} }
@@ -62,7 +62,7 @@
<p> <p>
<span class="strong">Step 3:</span> Merge the changes and update the server <span class="strong">Step 3:</span> Merge the changes and update the server
</p> </p>
@defining(s"git checkout master\ngit merge ${pullreq.requestUserName}-${pullreq.branch}\ngit push origin ${pullreq.branch}"){ command => @defining(s"git checkout master\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-3", command){ @helper.html.copy("merge-command-copy-3", command){
<pre style="width: 500px; float: left;">@command</pre> <pre style="width: 500px; float: left;">@command</pre>
} }

View File

@@ -33,13 +33,13 @@
} }
</div> </div>
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab"> <ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
<li class="active"><a href="#discussion">Discussion</a></li> <li class="active"><a href="#conversation">Conversation <span class="badge">@comments.size</span></a></li>
<li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li> <li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li>
<li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li> <li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li>
</ul> </ul>
<div class="tab-content fill-width pull-left"> <div class="tab-content fill-width pull-left">
<div class="tab-pane active" id="discussion"> <div class="tab-pane active" id="conversation">
@pulls.html.discussion(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) @pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
<div class="tab-pane" id="commits"> <div class="tab-pane" id="commits">
@pulls.html.commits(dayByDayCommits, repository) @pulls.html.commits(dayByDayCommits, repository)

View File

@@ -33,8 +33,8 @@
<th style="font-weight: normal;"> <th style="font-weight: normal;">
<div class="pull-left"> <div class="pull-left">
@avatar(latestCommit, 20) @avatar(latestCommit, 20)
@user(latestCommit.committer, latestCommit.mailAddress, "username strong") @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")
<span class="muted">@datetime(latestCommit.time)</span> <span class="muted">@datetime(latestCommit.commitTime)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a> <a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
</div> </div>
<div class="btn-group pull-right"> <div class="btn-group pull-right">
@@ -77,7 +77,7 @@
</table> </table>
} }
} }
<script src="@assets/common/js/jquery.ba-hashchange.js"></script> <script src="@assets/vendors/jquery/jquery.ba-hashchange.js"></script>
<script> <script>
$(window).load(function(){ $(window).load(function(){
$(window).hashchange(function(){ $(window).hashchange(function(){

View File

@@ -31,7 +31,10 @@
<a href="@url(repository)/compare/@{encodeRefName(repository.repository.defaultBranch)}...@{encodeRefName(branchName)}">to @{repository.repository.defaultBranch}</a> <a href="@url(repository)/compare/@{encodeRefName(repository.repository.defaultBranch)}...@{encodeRefName(branchName)}">to @{repository.repository.defaultBranch}</a>
} }
</td> </td>
<td><a href="@url(repository)/archive/@{encodeRefName(branchName)}.zip">ZIP</a></td> <td>
<a href="@url(repository)/archive/@{encodeRefName(branchName)}.zip">ZIP</a>
<a href="@url(repository)/archive/@{encodeRefName(branchName)}.tar.gz">TAR.GZ</a>
</td>
</tr> </tr>
} }
</table> </table>

View File

@@ -42,9 +42,6 @@
</tr> </tr>
<tr> <tr>
<td> <td>
@avatar(commit, 20)
@user(commit.committer, commit.mailAddress, "username strong")
<span class="muted">@datetime(commit.time)</span>
<div class="pull-right monospace small" style="text-align: right;"> <div class="pull-right monospace small" style="text-align: right;">
<div> <div>
@if(commit.parents.size == 0){ @if(commit.parents.size == 0){
@@ -66,6 +63,21 @@
</div> </div>
} }
</div> </div>
<div class="author-info">
<div class="author">
@avatar(commit, 20)
<span>@user(commit.authorName, commit.authorEmailAddress, "username strong")</span>
<span class="muted">authored on @datetime(commit.authorTime)</span>
</div>
@if(commit.isDifferentFromAuthor) {
<div class="committer">
<span class="icon-arrow-right"></span>
<span>@user(commit.committerName, commit.committerEmailAddress, "username strong")</span>
<span class="muted"> committed on @datetime(commit.commitTime)</span>
</div>
}
</div>
</td> </td>
</tr> </tr>
</table> </table>

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