Compare commits

...

79 Commits
2.0 ... 2.1

Author SHA1 Message Date
Naoki Takezoe
104c3bc89d Disable TestCase for Services 2014-07-06 17:35:18 +09:00
Naoki Takezoe
2668977918 Convert CRLF to LF 2014-07-06 17:07:52 +09:00
Naoki Takezoe
28424c96c4 Readying 2.1 release 2014-07-06 17:06:26 +09:00
Naoki Takezoe
9cfa8c594b Merge branch 'master' into slick2
Conflicts:
	project/build.scala
	src/main/scala/app/IndexController.scala
	src/main/scala/app/RepositorySettingsController.scala
	src/main/scala/model/Account.scala
	src/main/scala/model/BasicTemplate.scala
	src/main/scala/model/Issue.scala
	src/main/scala/model/IssueComment.scala
	src/main/scala/model/package.scala
	src/main/scala/service/IssuesService.scala
	src/main/scala/service/PullRequestService.scala
	src/main/scala/service/RepositoryService.scala
	src/main/scala/service/WikiService.scala
	src/main/scala/servlet/TransactionFilter.scala
	src/main/scala/util/Notifier.scala
2014-07-06 17:02:49 +09:00
Naoki Takezoe
5c70cd654c (refs #341)Add TODO about Slick 2.0 migration 2014-07-06 16:21:25 +09:00
Naoki Takezoe
7aca24e51d (refs #341)Fix compilation error about date conversion 2014-07-06 16:09:50 +09:00
Naoki Takezoe
cce0b67871 (refs #341)Fix compilation error of delete statements 2014-07-06 15:42:45 +09:00
Naoki Takezoe
606cd83f44 Fix CRLF to LF 2014-07-06 13:19:27 +09:00
Naoki Takezoe
32897c36f9 (refs #341)Fix compilation error other than date mapping and delete statement 2014-07-06 13:07:05 +09:00
Naoki Takezoe
92e4e12655 Update README.md 2014-07-05 18:06:51 +09:00
Naoki Takezoe
c8e5b75165 Disable front-end of plugin system in GitBucket 2.1 2014-07-05 18:01:19 +09:00
Naoki Takezoe
09b9a52ad3 Merge branch 'plugin-system' 2014-07-05 17:36:02 +09:00
Naoki Takezoe
33378c6464 Merge pull request #425 from okapies/fix-ldap-filter
Filter by username explicitly
2014-07-05 11:54:08 +09:00
shimamoto
259bcfc14f (refs #341) Fix compile errors. 2014-07-01 04:08:15 +09:00
shimamoto
c361d24ba4 (refs #341) Implement a method to get the session. 2014-07-01 03:43:55 +09:00
shimamoto
d5e1b18b52 (refs #341) Sets default value. 2014-07-01 03:40:21 +09:00
tanacasino
684a17a15b Merge pull request #422 from tanacasino/improve-markdown-css
Make markdown looks like more GitHub
2014-07-01 02:44:11 +09:00
Yuta Okamoto
66b7b69d20 specify LDAP search filter explicitly 2014-06-30 23:38:43 +09:00
Tomofumi Tanaka
57254f6366 Make markdown looks like more GitHub 2014-06-29 22:17:12 +09:00
Naoki Takezoe
c64909ab1a Plugin updating is executed asynchronously by Quartz Scheduler 2014-06-29 16:09:41 +09:00
Naoki Takezoe
34dd8541f4 Disable JavaScript console 2014-06-29 14:35:49 +09:00
Naoki Takezoe
50b4fb154d Fix JavaScript path 2014-06-29 14:33:48 +09:00
Naoki Takezoe
0b3781ec8a Add plugin updating capability 2014-06-29 14:26:33 +09:00
Naoki Takezoe
0e1d184715 Remove installed plugins from available plugin list 2014-06-29 12:39:27 +09:00
Naoki Takezoe
d8c27046f6 Merge branch 'master' into plugin-system
Conflicts:
	src/main/scala/servlet/AutoUpdateListener.scala
2014-06-29 04:20:58 +09:00
Naoki Takezoe
fd09058a7d (refs #310)Convert all CRLF to LF 2014-06-29 04:16:37 +09:00
Naoki Takezoe
1c99b57709 (refs #412)Fix repository lock 2014-06-29 03:29:03 +09:00
shimamoto
9ee739d102 (refs #341) Migrate slick session. 2014-06-27 08:48:58 +09:00
Tomofumi Tanaka
e2cde81b72 (refs #417) Correct wiki repository url 2014-06-25 23:40:41 +09:00
Tomofumi Tanaka
84a4b8fd92 (refs #415) Fix bug 2014-06-25 00:19:28 +09:00
shimamoto
d2c94909cb (refs #341) Migrate service package. 2014-06-24 02:40:40 +09:00
Naoki Takezoe
a9bfe0dfab (refs #411)Move thirdparty JavaScript and CSS to vendors/ 2014-06-20 11:51:27 +09:00
Naoki Takezoe
9af81c7093 Merge pull request #410 from cranst0n/ant-deprecation
Remove deprecated Ant 'rename' task.
2014-06-20 09:25:43 +09:00
cranst0n
1e8a5c3cde Remove deprecated 'rename' task. 2014-06-19 13:21:24 -04:00
Naoki Takezoe
707ad866e1 (refs #408)Performance improvement for index page. 2014-06-20 01:42:42 +09:00
Naoki Takezoe
c3a944b40e (refs #405)Fix styles 2014-06-16 10:59:21 +09:00
Naoki Takezoe
ab80cb8f60 (refs #405)Fix styles 2014-06-16 10:39:27 +09:00
Naoki Takezoe
4f45e047d2 (refs #406)Fix pull request count in the dashboard 2014-06-16 01:40:51 +09:00
shimamoto
bbe455ac49 (refs #341) Migrate model package. 2014-06-16 01:29:56 +09:00
Naoki Takezoe
b5f173fa46 (refs #32)Display plugin's status 2014-06-16 01:20:42 +09:00
Naoki Takezoe
4bd6ef143a (refs #32)Clone or pull plugin repository before displaying the available plugins page 2014-06-15 18:07:34 +09:00
Naoki Takezoe
fd4a696303 (refs #32)Add version to plugin meta information 2014-06-15 17:39:42 +09:00
Naoki Takezoe
4af4c4e7c6 (refs #32)Add plugin install tab 2014-06-15 13:11:08 +09:00
Naoki Takezoe
3b2e42fd61 (refs #32)Making plugin administration pages 2014-06-14 23:29:37 +09:00
Naoki Takezoe
b07d0b028f (refs #32)Add deleting installed plugins 2014-06-14 14:32:40 +09:00
Naoki Takezoe
f3900ca8f9 (refs #32)Add plugin system initialization 2014-06-14 14:00:21 +09:00
Naoki Takezoe
62d43f120a (refs #32)Separate Plugin interface and implementation 2014-06-14 13:26:56 +09:00
Naoki Takezoe
c4f69fbd13 (refs #32)Switch JavaScript processor to Rhino from Nashorn because GitBucket should work on both of JDK7 and JDK8 2014-06-14 10:58:58 +09:00
Naoki Takezoe
fece20ff40 (refs #32)Implementing repository action processing 2014-06-11 01:45:51 +09:00
Naoki Takezoe
bbef4b22ca (refs #32)Add plugins page into the system admin tools 2014-06-10 10:44:36 +09:00
Naoki Takezoe
481a2d213f (refs #32)Improving plug-in API 2014-06-10 07:43:52 +09:00
Tomofumi Tanaka
8ed4075f1e Change word to clear milestone the same as GitHub 2014-06-10 00:40:09 +09:00
Tomofumi Tanaka
9bf82733d1 Make icon-remove-circle of dropdown menu unclickable 2014-06-10 00:24:30 +09:00
Tomofumi Tanaka
30d66f95bc Show number of conversations 2014-06-09 22:49:41 +09:00
Naoki Takezoe
378c2c39a8 (refs #32)Add JavaScript API 2014-06-08 00:37:01 +09:00
Naoki Takezoe
daf5fc434c Merge remote-tracking branch 'origin/plugin-system' into plugin-system 2014-06-07 18:53:31 +09:00
Naoki Takezoe
e5bf90ed26 (refs #32)Provide generic layout and context to custom actions 2014-06-07 18:53:07 +09:00
Tomofumi Tanaka
1bf3146220 (refs #396)Apply syntax highlight correctly when update comment 2014-06-06 22:22:51 +09:00
Naoki Takezoe
ddd51850f0 (refs #32)Add JavaScript Console 2014-06-06 17:20:48 +09:00
Naoki Takezoe
e14a0c3770 (refs #32)Enable menu icon which is injected by plug-in 2014-06-06 16:35:54 +09:00
Naoki Takezoe
b0b318ce30 (refs #32)Example of custom action extension 2014-06-05 21:22:16 +09:00
Naoki Takezoe
6f666ca49f Merge branch 'master' into plugin-system 2014-06-05 20:54:28 +09:00
Naoki Takezoe
0cb2116bdf (refs #32)First impression of the plugin system 2014-06-05 20:52:38 +09:00
Naoki Takezoe
280113497b Merge pull request #390 from sakapoko/navform
Fix nested tags.
2014-06-05 09:37:40 +09:00
Shuji Sakagami
5f6e318329 Fix nested tags. 2014-06-04 13:11:37 +09:00
takezoe
f8921b6f10 Add a badge which shows number of pages 2014-06-03 07:28:30 +09:00
Naoki Takezoe
31a08abff2 (refs #378)"Delete branch" button is displayed for only merged pull requests 2014-06-02 21:34:03 +09:00
Naoki Takezoe
0fa1e11c5a (refs #378)Closed but not merged pull requests should be re-openable 2014-06-02 21:28:20 +09:00
Naoki Takezoe
1edff41690 Fix code style 2014-06-02 16:10:03 +09:00
Naoki Takezoe
6d6f529d40 (refs #380)Close stream certainly 2014-06-02 16:04:45 +09:00
Naoki Takezoe
e2fd7d9d8e Fix "New pull request" button style 2014-06-01 23:46:28 +09:00
Naoki Takezoe
61146687b3 Merge remote-tracking branch 'origin/master' 2014-06-01 22:58:24 +09:00
Naoki Takezoe
1d1f7fa581 (refs #382)Remove unnecessary comment 2014-06-01 22:58:07 +09:00
Tomofumi Tanaka
67da88fab5 Use "Conversation" instead of "Discussion"
Github uses "Conversation" now.
2014-06-01 22:45:10 +09:00
Naoki Takezoe
fb3ed70215 (refs #382)Exclude duplicated commits from applying issue comment 2014-06-01 21:47:45 +09:00
Naoki Takezoe
2fceeeee4e Merge pull request #386 from HairyFotr/patch-4
Small cleanup using static analysis
2014-05-31 17:26:23 +09:00
Naoki Takezoe
67102822e8 Update README.md 2014-05-31 13:17:21 +09:00
Naoki Takezoe
d00a0f1571 Update README.md 2014-05-31 13:16:53 +09:00
HairyFotr
7698f12112 Small cleanup using static analysis 2014-05-31 00:57:03 +02:00
339 changed files with 100620 additions and 63506 deletions

View File

@@ -80,6 +80,16 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.1 - 6 Jul 2014
- Upgrade to Slick 2.0 from 1.9
- Base part of the plug-in system is merged
- Many bug fix and improvements
### 2.0 - 31 May 2014
- Modern Github UI
- Preview in AceEditor
- Select lines by clicking line number in blob view
### 1.13 - 29 Apr 2014 ### 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

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

@@ -38,8 +38,10 @@ object MyBuild extends Build {
"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.0.2",
"org.mozilla" % "rhino" % "1.7R4",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.3.173", "com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._ import service.IssuesService._
import service.PullRequestService._ import service.PullRequestService._
import util.JGitUtil.DiffInfo import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
@@ -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.
@@ -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)
@@ -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

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

View File

@@ -255,7 +255,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val name = multiParams("splat").head val name = multiParams("splat").head
if(name.endsWith(".zip")){ if(name.endsWith(".zip")){
val revision = name.replaceFirst("\\.zip$", "") val revision = name.stripSuffix(".zip")
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists){ if(workDir.exists){
FileUtils.deleteDirectory(workDir) FileUtils.deleteDirectory(workDir)
@@ -316,9 +316,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("/"))
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
package model
import slick.driver.JdbcProfile
trait Profile {
val profile: JdbcProfile
import profile.simple._
// java.util.Date Mapped Column Types
implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp](
d => new java.sql.Timestamp(d.getTime),
t => new java.util.Date(t.getTime)
)
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
package service package service
import model._ import model._
import scala.slick.driver.H2Driver.simple._ import simple._
import Database.threadLocalSession
import util.JGitUtil import util.JGitUtil
trait RepositoryService { self: AccountService => trait RepositoryService { self: AccountService =>
@@ -20,7 +19,8 @@ trait RepositoryService { self: AccountService =>
*/ */
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None, originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = { parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
(implicit s: Session): Unit = {
Repositories insert Repositories insert
Repository( Repository(
userName = userName, userName = userName,
@@ -39,33 +39,34 @@ 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 is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t => Repositories.filter { t =>
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind) (t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t => PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind t.requestRepositoryName is oldRepositoryName.bind
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName) }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
deleteRepository(oldUserName, oldRepositoryName) deleteRepository(oldUserName, oldRepositoryName)
@@ -88,7 +89,7 @@ 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 is activityId.bind).map(_.message).update(
@@ -104,7 +105,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 +125,8 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of repository owner * @param userName the user name of repository owner
* @return the list of repository names * @return the list of repository names
*/ */
def getRepositoryNamesOfUser(userName: String): List[String] = def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list Repositories filter(_.userName is userName.bind) map (_.repositoryName) list
/** /**
* Returns the specified repository information. * Returns the specified repository information.
@@ -135,10 +136,10 @@ trait RepositoryService { self: AccountService =>
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @return the repository information * @return the repository information
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count // for getting issue count and pull request count
val issues = Query(Issues).filter { t => val issues = Issues.filter { t =>
t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind) t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
}.map(_.pullRequest).list }.map(_.pullRequest).list
@@ -155,13 +156,18 @@ trait RepositoryService { self: AccountService =>
} }
} }
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = { def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
Query(Repositories).filter { t1 => (implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName is userName.bind) || (t1.userName is userName.bind) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists) (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
@@ -178,24 +184,32 @@ trait RepositoryService { self: AccountService =>
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = { def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories) case Some(x) if(x.isAdmin) => Repositories
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) || Repositories filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists) (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
} }
// for Guests // for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind) case None => Repositories filter(_.isPrivate is false.bind)
}).filter { t => }).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE repositoryUserName.map { userName => t.userName is userName.bind } getOrElse LiteralColumn(true)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
@@ -205,7 +219,7 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getRepositoryManagers(userName: String): Seq[String] = private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){ if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else { } else {
@@ -215,16 +229,16 @@ trait RepositoryService { self: AccountService =>
/** /**
* Updates the last activity date of the repository. * Updates the last activity date of the repository.
*/ */
def updateLastActivityDate(userName: String, repositoryName: String): Unit = def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate) Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
/** /**
* Save repository options. * Save repository options.
*/ */
def saveRepositoryOptions(userName: String, repositoryName: String, def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], defaultBranch: String, isPrivate: Boolean): Unit = description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)) Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => r.description.? ~ r.defaultBranch ~ r.isPrivate ~ r.updatedDate } .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
.update (description, defaultBranch, isPrivate, currentDate) .update (description, defaultBranch, isPrivate, currentDate)
/** /**
@@ -234,8 +248,8 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators insert(Collaborator(userName, repositoryName, collaboratorName)) Collaborators insert Collaborator(userName, repositoryName, collaboratorName)
/** /**
* Remove collaborator from the repository. * Remove collaborator from the repository.
@@ -244,7 +258,7 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @param collaboratorName the collaborator name * @param collaboratorName the collaborator name
*/ */
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/** /**
@@ -253,7 +267,7 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param repositoryName the repository name * @param repositoryName the repository name
*/ */
def removeCollaborators(userName: String, repositoryName: String): Unit = def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/** /**
@@ -263,10 +277,10 @@ trait RepositoryService { self: AccountService =>
* @param repositoryName the repository name * @param repositoryName the repository name
* @return the list of collaborators name * @return the list of collaborators name
*/ */
def getCollaborators(userName: String, repositoryName: String): List[String] = def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] =
Query(Collaborators).filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account]): Boolean = { def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match { loginAccount match {
case Some(a) if(a.isAdmin) => true case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true case Some(a) if(a.userName == owner) => true
@@ -275,17 +289,18 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getForkedCount(userName: String, repositoryName: String): Int = private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
// TODO check SQL
Query(Repositories.filter { t => Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.length).first }.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] = def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
Query(Repositories).filter { t => Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
} }
.sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,9 @@ package servlet
import javax.servlet._ import javax.servlet._
import javax.servlet.http._ import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService} import service.{SystemSettingsService, AccountService, RepositoryService}
import model.Account import model._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import slick.jdbc.JdbcBackend
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import util.Keys import util.Keys
@@ -21,7 +22,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
def destroy(): Unit = {} def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
val request = req.asInstanceOf[HttpServletRequest] implicit val request = req.asInstanceOf[HttpServletRequest]
val response = res.asInstanceOf[HttpServletResponse] val response = res.asInstanceOf[HttpServletResponse]
val wrappedResponse = new HttpServletResponseWrapper(response){ val wrappedResponse = new HttpServletResponseWrapper(response){
@@ -65,7 +66,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: JdbcBackend#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 slick.jdbc.JdbcBackend
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -64,7 +65,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
@@ -76,14 +77,16 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "") val repository = paths(2).stripSuffix(".git")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
val hook = new CommitLogHook(owner, repository, pusher, baseUrl(request)) defining(request) { implicit r =>
receivePack.setPreReceiveHook(hook) val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPostReceiveHook(hook) receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
} }
receivePack receivePack
} }
@@ -92,7 +95,8 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook with PreReceiveHook class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: JdbcBackend#Session)
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -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) =>

View File

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

View File

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

View File

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

View File

@@ -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.
@@ -146,12 +150,12 @@ object JGitUtil {
commitCount, commitCount,
// branches // branches
git.branchList.call.asScala.map { ref => git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "") ref.getName.stripPrefix("refs/heads/")
}.toList, }.toList,
// tags // tags
git.tagList.call.asScala.map { ref => git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId) val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName) TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList }.toList
) )
} catch { } catch {
@@ -190,7 +194,7 @@ object JGitUtil {
val targetPath = walker.getPathString val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){ if((path + "/").startsWith(targetPath)){
true true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){ } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){
stopRecursive = true stopRecursive = true
treeWalk.setRecursive(false) treeWalk.setRecursive(false)
true true

View File

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

View File

@@ -47,11 +47,11 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""), keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed." error = "User LDAP Authentication Failed."
){ conn => ){ conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(LDAPUserInfo( case Some(mailAddress) => Right(LDAPUserInfo(
userName = 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.")
@@ -130,15 +130,15 @@ object LDAPUtil {
} }
} }
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None } else None
} }
private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] = private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None } else None

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

@@ -1,105 +1,105 @@
@(diffs: Seq[util.JGitUtil.DiffInfo], @(diffs: Seq[util.JGitUtil.DiffInfo],
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
newCommitId: Option[String], newCommitId: Option[String],
oldCommitId: Option[String], oldCommitId: Option[String],
showIndex: Boolean)(implicit context: app.Context) showIndex: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType @import org.eclipse.jgit.diff.DiffEntry.ChangeType
@if(showIndex){ @if(showIndex){
<div> <div>
<div class="pull-right" style="margin-bottom: 10px;"> <div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/> <input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div> </div>
Showing @diffs.size changed @plural(diffs.size, "file") Showing @diffs.size changed @plural(diffs.size, "file")
</div> </div>
<ul id="commit-file-list" style="display: none;"> <ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}> <li@if(i > 0){ class="border"}>
<a href="#diff-@i"> <a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath <img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
} }
@if(diff.changeType == ChangeType.ADD){ @if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath <img src="@assets/common/images/diff_add.png"/> @diff.newPath
} }
@if(diff.changeType == ChangeType.MODIFY){ @if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath <img src="@assets/common/images/diff_edit.png"/> @diff.newPath
} }
@if(diff.changeType == ChangeType.DELETE){ @if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath <img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
} }
</a> </a>
</li> </li>
} }
</ul> </ul>
} }
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
<a name="diff-@i"></a> <a name="diff-@i"></a>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th style="font-weight: normal;" class="box-header"> <th style="font-weight: normal;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath @diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){ @if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@diff.newPath @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
@if(diff.changeType == ChangeType.DELETE){ @if(diff.changeType == ChangeType.DELETE){
@diff.oldPath @diff.oldPath
@if(oldCommitId.isDefined){ @if(oldCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
</th> </th>
</tr> </tr>
<tr> <tr>
<td> <td>
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i"></div> <div id="diffText-@i"></div>
<textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea> <textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea>
<textarea id="oldText-@i" style="display: none;">@diff.oldContent.getOrElse("")</textarea> <textarea id="oldText-@i" style="display: none;">@diff.oldContent.getOrElse("")</textarea>
} else { } else {
Not supported Not supported
} }
</td> </td>
</tr> </tr>
</table> </table>
} }
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script> <script>
$(function(){ $(function(){
@if(showIndex){ @if(showIndex){
$('#toggle-file-list').click(function(){ $('#toggle-file-list').click(function(){
$('#commit-file-list').toggle(); $('#commit-file-list').toggle();
if($(this).val() == 'Show file list'){ if($(this).val() == 'Show file list'){
$(this).val('Hide file list'); $(this).val('Hide file list');
} else { } else {
$(this).val('Show file list'); $(this).val('Show file list');
} }
}); });
} }
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){ if($('#oldText-@i').length > 0){
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i'); diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i');
} }
} }
} }
}); });
</script> </script>

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,70 +1,70 @@
@(activities: List[model.Activity], @(activities: List[model.Activity],
recentRepositories: List[service.RepositoryService.RepositoryInfo], recentRepositories: List[service.RepositoryService.RepositoryInfo],
userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main("GitBucket"){ @main("GitBucket"){
<div class="container"> <div class="container">
@dashboard.html.tab() @dashboard.html.tab()
<div class="row-fluid"> <div class="row-fluid">
<div class="span8"> <div class="span8">
@helper.html.activities(activities) @helper.html.activities(activities)
</div> </div>
<div class="span4"> <div class="span4">
@if(loginAccount.isEmpty){ @if(loginAccount.isEmpty){
@signinform(settings) @signinform(settings)
} else { } else {
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th class="metal"> <th class="metal">
<div class="pull-right"> <div class="pull-right">
<a href="@path/new" class="btn btn-success btn-mini">New repository</a> <a href="@path/new" class="btn btn-success btn-mini">New repository</a>
</div> </div>
Your repositories (@userRepositories.size) Your repositories (@userRepositories.size)
</th> </th>
</tr> </tr>
@if(userRepositories.isEmpty){ @if(userRepositories.isEmpty){
<tr> <tr>
<td>No repositories</td> <td>No repositories</td>
</tr> </tr>
} else { } else {
@userRepositories.map { repository => @userRepositories.map { repository =>
<tr> <tr>
<td> <td>
@helper.html.repositoryicon(repository, false) @helper.html.repositoryicon(repository, false)
@if(repository.owner == loginAccount.get.userName){ @if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a> <a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else { } else {
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
} }
</td> </td>
</tr> </tr>
} }
} }
</table> </table>
} }
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th class="metal"> <th class="metal">
Recent updated repositories Recent updated repositories
</th> </th>
</tr> </tr>
@if(recentRepositories.isEmpty){ @if(recentRepositories.isEmpty){
<tr> <tr>
<td>No repositories</td> <td>No repositories</td>
</tr> </tr>
} else { } else {
@recentRepositories.map { repository => @recentRepositories.map { repository =>
<tr> <tr>
<td> <td>
@helper.html.repositoryicon(repository, false) @helper.html.repositoryicon(repository, false)
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td> </td>
</tr> </tr>
} }
} }
</table> </table>
</div> </div>
</div> </div>
</div> </div>
} }

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

@@ -1,147 +1,147 @@
@(collaborators: List[String], @(collaborators: List[String],
milestones: List[model.Milestone], milestones: List[model.Milestone],
labels: List[model.Label], labels: List[model.Label],
hasWritePermission: Boolean, hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("", true, repository) @tab("", true, repository)
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span9"> <div class="span9">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box"> <div class="box issue-box">
<div class="box-content"> <div class="box-content">
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;"/> <input type="text" name="title" value="" placeholder="Title" style="width: 565px;"/>
<div> <div>
<span id="label-assigned">No one is assigned</span> <span id="label-assigned">No one is assigned</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="assignedUserName" value=""/> <input type="hidden" name="assignedUserName" value=""/>
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li> <li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li> <li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
} }
} }
} }
<div class="pull-right"> <div class="pull-right">
<span id="label-milestone">No milestone</span> <span id="label-milestone">No milestone</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/> <input type="hidden" name="milestoneId" value=""/>
@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> No milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone => @milestones.filter(_.closedDate.isEmpty).map { 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">
<i class="icon-while"></i> @milestone.title <i class="icon-while"></i> @milestone.title
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate) <img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else { } else {
<span class="muted">Due in @date(dueDate)</span> <span class="muted">Due in @date(dueDate)</span>
} }
}.getOrElse { }.getOrElse {
<span class="muted">No due date</span> <span class="muted">No due date</span>
} }
</div> </div>
</a> </a>
</li> </li>
} }
} }
} }
</div> </div>
</div> </div>
<hr> <hr>
@helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) @helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/> <input type="submit" class="btn btn-success" value="Submit new issue"/>
</div> </div>
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
<span class="strong">Add Labels</span> <span class="strong">Add Labels</span>
<div> <div>
<div id="label-list"> <div id="label-list">
<ul class="label-list nav nav-pills nav-stacked"> <ul class="label-list nav nav-pills nav-stacked">
@labels.map { label => @labels.map { label =>
<li> <li>
<a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor"> <a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor">
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span> <span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName @label.labelName
</a> </a>
</li> </li>
} }
</ul> </ul>
<input type="hidden" name="labelNames" value=""/> <input type="hidden" name="labelNames" value=""/>
</div> </div>
</div> </div>
} }
</div> </div>
</div> </div>
</form> </form>
} }
} }
<script> <script>
$(function(){ $(function(){
$('a.assign').click(function(){ $('a.assign').click(function(){
var userName = $(this).data('name'); var userName = $(this).data('name');
$('a.assign i.icon-ok').attr('class', 'icon-white'); $('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){ if(userName == ''){
$('#label-assigned').text('No one will be assigned'); $('#label-assigned').text('No one will be assigned');
} else { } else {
$('#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);
}); });
$('a.milestone').click(function(){ $('a.milestone').click(function(){
var title = $(this).data('title'); var title = $(this).data('title');
var milestoneId = $(this).data('id'); var milestoneId = $(this).data('id');
$('a.milestone i.icon-ok').attr('class', 'icon-white'); $('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){ if(milestoneId == ''){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title))); $('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }
$('input[name=milestoneId]').val(milestoneId); $('input[name=milestoneId]').val(milestoneId);
}); });
$('a.toggle-label').click(function(){ $('a.toggle-label').click(function(){
if($(this).data('selected') == true){ if($(this).data('selected') == true){
$(this).css({ $(this).css({
'background-color': 'white', 'background-color': 'white',
'color' : 'black', 'color' : 'black',
'font-weight' : 'normal' 'font-weight' : 'normal'
}); });
$(this).data('selected', false); $(this).data('selected', false);
} else { } else {
$(this).css({ $(this).css({
'background-color': '#' + $(this).data('bgcolor'), 'background-color': '#' + $(this).data('bgcolor'),
'color' : '#' + $(this).data('fgcolor'), 'color' : '#' + $(this).data('fgcolor'),
'font-weight' : 'bold' 'font-weight' : 'bold'
}); });
$(this).data('selected', true); $(this).data('selected', true);
} }
var labelNames = Array(); var labelNames = Array();
$('a.toggle-label').each(function(i, e){ $('a.toggle-label').each(function(i, e){
if($(e).data('selected') == true){ if($(e).data('selected') == true){
labelNames.push($(e).data('label')); labelNames.push($(e).data('label'));
} }
}); });
$('input[name=labelNames]').val(labelNames.join(',')); $('input[name=labelNames]').val(labelNames.join(','));
}); });
}); });
</script> </script>

View File

@@ -12,6 +12,7 @@
$(function(){ $(function(){
var callback = function(data){ var callback = function(data){
$('#commentContent-@commentId').empty().html(data.content); $('#commentContent-@commentId').empty().html(data.content);
prettyPrint();
}; };
$('#update-comment-@commentId').click(function(){ $('#update-comment-@commentId').click(function(){

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")
} }

View File

@@ -1,25 +1,25 @@
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]], @(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="box"> <div class="box">
<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.time)</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.committer, commit.mailAddress, "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;">
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a> <a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
</td> </td>
</tr> </tr>
} }
} }
</table> </table>
</div> </div>

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

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

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

@@ -51,9 +51,9 @@
</form> </form>
} }
} }
<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(){
diffUsingJS('oldText', 'newText', 'diffText'); diffUsingJS('oldText', 'newText', 'diffText');

View File

@@ -68,10 +68,10 @@
</form> </form>
} }
} }
<script src="@assets/ace/ace.js" type="text/javascript" charset="utf-8"></script> <script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<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(){
$('#editor').text($('#initial').val()); $('#editor').text($('#initial').val());

View File

@@ -1,24 +1,24 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){ @html.menu("code", repository){
<h1>Tags</h1> <h1>Tags</h1>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th width="40%">Tag</th> <th width="40%">Tag</th>
<th width="20%">Date</th> <th width="20%">Date</th>
<th width="20%">Commit</th> <th width="20%">Commit</th>
<th width="20%">Download</th> <th width="20%">Download</th>
</tr> </tr>
@repository.tags.map { tag => @repository.tags.map { tag =>
<tr> <tr>
<td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td> <td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td>
<td>@datetime(tag.time)</td> <td>@datetime(tag.time)</td>
<td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td> <td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td>
<td><a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a></td> <td><a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a></td>
</tr> </tr>
} }
</table> </table>
} }
} }

View File

@@ -1,38 +1,38 @@
@(active: String, fileCount: Int, issueCount: Int, query: String, @(active: String, fileCount: Int, issueCount: Int, query: String,
repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.menu("", repository){ @html.menu("", repository){
<div class="row-fluid"> <div class="row-fluid">
<div class="span3"> <div class="span3">
<div class="box"> <div class="box">
<ul class="nav nav-tabs nav-stacked side-menu"> <ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="code"){ class="active"}> <li@if(active=="code"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=code"> <a href="@url(repository)/search?q=@urlEncode(query)&type=code">
@if(fileCount != 0){ @if(fileCount != 0){
<span class="badge pull-right">@fileCount</span> <span class="badge pull-right">@fileCount</span>
} }
Code Code
</a> </a>
</li> </li>
<li@if(active=="issue"){ class="active"}> <li@if(active=="issue"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=issue"> <a href="@url(repository)/search?q=@urlEncode(query)&type=issue">
@if(issueCount != 0){ @if(issueCount != 0){
<span class="badge pull-right">@issueCount</span> <span class="badge pull-right">@issueCount</span>
} }
Issue Issue
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="span9"> <div class="span9">
<form action="@url(repository)/search" method="GET"> <form action="@url(repository)/search" method="GET">
<input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/> <input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/>
<input type="submit" value="Search" class="btn" style="width: 15%;"/> <input type="submit" value="Search" class="btn" style="width: 15%;"/>
<input type="hidden" name="type" value="@active"/> <input type="hidden" name="type" value="@active"/>
</form> </form>
@body @body
</div> </div>
</div> </div>
} }

View File

@@ -1,35 +1,35 @@
@(collaborators: List[String], @(collaborators: List[String],
isGroupRepository: Boolean, isGroupRepository: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Settings", Some(repository)){ @html.main("Settings", Some(repository)){
@html.menu("settings", repository){ @html.menu("settings", repository){
@menu("collaborators", repository){ @menu("collaborators", repository){
<h3>Manage Collaborators</h3> <h3>Manage Collaborators</h3>
<ul class="collaborator"> <ul class="collaborator">
@collaborators.map { collaboratorName => @collaborators.map { collaboratorName =>
<li> <li>
<a href="@url(collaboratorName)">@collaboratorName</a> <a href="@url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){ @if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a> <a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else { } else {
@if(repository.managers.contains(collaboratorName)){ @if(repository.managers.contains(collaboratorName)){
(Manager) (Manager)
} }
} }
</li> </li>
} }
</ul> </ul>
@if(!isGroupRepository){ @if(!isGroupRepository){
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true" autocomplete="off"> <form method="POST" action="@url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
<div> <div>
<span class="error" id="error-userName"></span> <span class="error" id="error-userName"></span>
</div> </div>
@helper.html.account("userName", 300) @helper.html.account("userName", 300)
<input type="submit" class="btn" value="Add"/> <input type="submit" class="btn" value="Add"/>
</form> </form>
} }
} }
} }
} }

View File

@@ -1,26 +1,26 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) @(active: String, repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="row-fluid"> <div class="row-fluid">
<div class="span3"> <div class="span3">
<div class="box"> <div class="box">
<ul class="nav nav-tabs nav-stacked side-menu"> <ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="options"){ class="active"}> <li@if(active=="options"){ class="active"}>
<a href="@url(repository)/settings/options">Options</a> <a href="@url(repository)/settings/options">Options</a>
</li> </li>
<li@if(active=="collaborators"){ class="active"}> <li@if(active=="collaborators"){ class="active"}>
<a href="@url(repository)/settings/collaborators">Collaborators</a> <a href="@url(repository)/settings/collaborators">Collaborators</a>
</li> </li>
<li@if(active=="hooks"){ class="active"}> <li@if(active=="hooks"){ class="active"}>
<a href="@url(repository)/settings/hooks">Service Hooks</a> <a href="@url(repository)/settings/hooks">Service Hooks</a>
</li> </li>
<li@if(active=="danger"){ class="active"}> <li@if(active=="danger"){ class="active"}>
<a href="@url(repository)/settings/danger">Danger Zone</a> <a href="@url(repository)/settings/danger">Danger Zone</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="span9"> <div class="span9">
@body @body
</div> </div>
</div> </div>

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