diff --git a/build.sbt b/build.sbt index 4b43aef..dcbe896 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,10 @@ val Organization = "gitbucket" val Name = "gitbucket" +<<<<<<< HEAD +val GitBucketVersion = "4.0.0-SNAPSHOT" +======= val GitBucketVersion = "3.14.0" +>>>>>>> master val ScalatraVersion = "2.4.0" val JettyVersion = "9.3.6.v20151106" @@ -15,7 +19,9 @@ // dependency settings resolvers ++= Seq( Classpaths.typesafeReleases, - "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/" + "amateras" at "http://amateras.sourceforge.jp/mvn/", + "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/", + "amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/" ) libraryDependencies ++= Seq( "org.scala-lang.modules" %% "scala-java8-compat" % "0.7.0", @@ -26,6 +32,7 @@ "org.json4s" %% "json4s-jackson" % "3.3.0", "io.github.gitbucket" %% "scalatra-forms" % "1.0.0", "commons-io" % "commons-io" % "2.4", + "io.github.gitbucket" % "solidbase" % "1.0.0", "io.github.gitbucket" % "markedj" % "1.0.8", "org.apache.commons" % "commons-compress" % "1.10", "org.apache.commons" % "commons-email" % "1.4", @@ -35,8 +42,10 @@ "com.typesafe.slick" %% "slick" % "2.1.0", "com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.4.190", + "mysql" % "mysql-connector-java" % "5.1.38", + "org.postgresql" % "postgresql" % "9.4.1208", "ch.qos.logback" % "logback-classic" % "1.1.1", - "com.mchange" % "c3p0" % "0.9.5.2", + "com.zaxxer" % "HikariCP" % "2.4.5", "com.typesafe" % "config" % "1.3.0", "com.typesafe.akka" %% "akka-actor" % "2.3.14", "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0", diff --git a/doc/release.md b/doc/release.md index 98ba383..d677a26 100644 --- a/doc/release.md +++ b/doc/release.md @@ -11,22 +11,24 @@ ```scala val Organization = "gitbucket" val Name = "gitbucket" -val GitBucketVersion = "3.12.0" // <---- update version!! +val GitBucketVersion = "4.0.0" // <---- update version!! val ScalatraVersion = "2.4.0" val JettyVersion = "9.3.6.v20151106" ``` -### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala +### src/main/scala/gitbucket/core/GitBucketCoreModule.scala ```scala -object AutoUpdate { - - /** - * The history of versions. A head of this sequence is the current GitBucket version. - */ - val versions = Seq( - new Version(3, 12), // <---- add this line!! - new Version(3, 11), +object GitBucketCoreModule extends Module("gitbucket-core", + // add new version definition + new Version("4.1.0", + new LiquibaseMigration("update/gitbucket-core_4.1.xml") + ), + new Version("4.0.0", + new LiquibaseMigration("update/gitbucket-core_4.0.xml"), + new SqlMigration("update/gitbucket-core_4.0.sql") + ) +) ``` Generate release files diff --git a/src/main/java/org/postgresql/Driver2.java b/src/main/java/org/postgresql/Driver2.java new file mode 100644 index 0000000..5763656 --- /dev/null +++ b/src/main/java/org/postgresql/Driver2.java @@ -0,0 +1,52 @@ +package org.postgresql; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +/** + * Wraps the PostgreSQL JDBC driver to convert the returning column names to lower case. + */ +public class Driver2 extends Driver { + + @Override + public java.sql.Connection connect(String url, Properties info) throws SQLException { + Connection conn = super.connect(url, info); + + Object proxy = Proxy.newProxyInstance( + conn.getClass().getClassLoader(), + new Class[]{ Connection.class }, + new ConnectionProxyHandler(conn) + ); + + return Connection.class.cast(proxy); + } + + + private static class ConnectionProxyHandler implements InvocationHandler { + + private Connection conn; + + public ConnectionProxyHandler(Connection conn){ + this.conn = conn; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if(method.getName().equals("prepareStatement")){ + if(args != null && args.length == 2 && args[1].getClass().isArray()){ + String[] keys = (String[]) args[1]; + for(int i = 0; i < keys.length; i++){ + keys[i] = keys[i].toLowerCase(); + } + } + } + return method.invoke(conn, args); + } + } + +} + diff --git a/src/main/resources/database.conf b/src/main/resources/database.conf deleted file mode 100644 index 0eca733..0000000 --- a/src/main/resources/database.conf +++ /dev/null @@ -1,6 +0,0 @@ -db { - driver = "org.h2.Driver" - url = "jdbc:h2:${DatabaseHome};MVCC=true" - user = "sa" - password = "sa" -} diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf deleted file mode 100644 index 2f8087c..0000000 --- a/src/main/resources/reference.conf +++ /dev/null @@ -1,5 +0,0 @@ -c3p0 { - privilegeSpawnedThreads=true - contextClassLoaderSource=library -} - diff --git a/src/main/resources/update/1_0.sql b/src/main/resources/update/1_0.sql deleted file mode 100644 index 7d64af6..0000000 --- a/src/main/resources/update/1_0.sql +++ /dev/null @@ -1,135 +0,0 @@ -CREATE TABLE ACCOUNT( - USER_NAME VARCHAR(100) NOT NULL, - MAIL_ADDRESS VARCHAR(100) NOT NULL, - PASSWORD VARCHAR(40) NOT NULL, - ADMINISTRATOR BOOLEAN NOT NULL, - URL VARCHAR(200), - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL, - LAST_LOGIN_DATE TIMESTAMP -); - -CREATE TABLE REPOSITORY( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - PRIVATE BOOLEAN NOT NULL, - DESCRIPTION TEXT, - DEFAULT_BRANCH VARCHAR(100), - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL, - LAST_ACTIVITY_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE COLLABORATOR( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - COLLABORATOR_NAME VARCHAR(100) NOT NULL -); - -CREATE TABLE ISSUE( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL, - OPENED_USER_NAME VARCHAR(100) NOT NULL, - MILESTONE_ID INT, - ASSIGNED_USER_NAME VARCHAR(100), - TITLE TEXT NOT NULL, - CONTENT TEXT, - CLOSED BOOLEAN NOT NULL, - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE ISSUE_ID( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL -); - -CREATE TABLE ISSUE_COMMENT( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL, - COMMENT_ID INT AUTO_INCREMENT, - ACTION VARCHAR(10), - COMMENTED_USER_NAME VARCHAR(100) NOT NULL, - CONTENT TEXT NOT NULL, - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE LABEL( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - LABEL_ID INT AUTO_INCREMENT, - LABEL_NAME VARCHAR(100) NOT NULL, - COLOR CHAR(6) NOT NULL -); - -CREATE TABLE ISSUE_LABEL( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL, - LABEL_ID INT NOT NULL -); - -CREATE TABLE MILESTONE( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - MILESTONE_ID INT AUTO_INCREMENT, - TITLE VARCHAR(100) NOT NULL, - DESCRIPTION TEXT, - DUE_DATE TIMESTAMP, - CLOSED_DATE TIMESTAMP -); - -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 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 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_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_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_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_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_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 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 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 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); - -INSERT INTO ACCOUNT ( - USER_NAME, - MAIL_ADDRESS, - PASSWORD, - ADMINISTRATOR, - URL, - REGISTERED_DATE, - UPDATED_DATE, - LAST_LOGIN_DATE -) VALUES ( - 'root', - 'root@localhost', - 'dc76e9f0c0006e8f919e0c515c66dbba3982f785', - true, - 'https://github.com/gitbucket/gitbucket', - SYSDATE, - SYSDATE, - NULL -); diff --git a/src/main/resources/update/1_1.sql b/src/main/resources/update/1_1.sql deleted file mode 100644 index 9cfd50a..0000000 --- a/src/main/resources/update/1_1.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Fix COLLABORATOR constraints -ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK1 IF EXISTS; -ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK0 IF EXISTS; -ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_PK IF EXISTS; - -ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COLLABORATOR_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); diff --git a/src/main/resources/update/1_12.sql b/src/main/resources/update/1_12.sql deleted file mode 100644 index f8658a2..0000000 --- a/src/main/resources/update/1_12.sql +++ /dev/null @@ -1,11 +0,0 @@ -ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE; - -CREATE TABLE SSH_KEY ( - USER_NAME VARCHAR(100) NOT NULL, - SSH_KEY_ID INT AUTO_INCREMENT, - TITLE VARCHAR(100) NOT NULL, - PUBLIC_KEY TEXT NOT NULL -); - -ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID); -ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME); diff --git a/src/main/resources/update/1_13.sql b/src/main/resources/update/1_13.sql deleted file mode 100644 index ed26f65..0000000 --- a/src/main/resources/update/1_13.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE COMMIT_LOG; \ No newline at end of file diff --git a/src/main/resources/update/1_2.sql b/src/main/resources/update/1_2.sql deleted file mode 100644 index d2765bc..0000000 --- a/src/main/resources/update/1_2.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TABLE ACTIVITY( - ACTIVITY_ID INT AUTO_INCREMENT, - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ACTIVITY_USER_NAME VARCHAR(100) NOT NULL, - ACTIVITY_TYPE VARCHAR(100) NOT NULL, - MESSAGE TEXT NOT NULL, - ADDITIONAL_INFO TEXT, - ACTIVITY_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE COMMIT_LOG ( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - COMMIT_ID VARCHAR(40) NOT NULL -); - -ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_PK PRIMARY KEY (ACTIVITY_ID); -ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); -ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK1 FOREIGN KEY (ACTIVITY_USER_NAME) REFERENCES ACCOUNT (USER_NAME); - -ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COMMIT_ID); -ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); - diff --git a/src/main/resources/update/1_3.sql b/src/main/resources/update/1_3.sql deleted file mode 100644 index 59ab009..0000000 --- a/src/main/resources/update/1_3.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TABLE ACCOUNT ADD COLUMN IMAGE VARCHAR(100); - -UPDATE ISSUE_COMMENT SET ACTION = 'comment' WHERE ACTION IS NULL; - -ALTER TABLE ISSUE_COMMENT ALTER COLUMN ACTION VARCHAR(20) NOT NULL; - -UPDATE ISSUE_COMMENT SET ACTION = 'close_comment' WHERE ACTION = 'close'; -UPDATE ISSUE_COMMENT SET ACTION = 'reopen_comment' WHERE ACTION = 'reopen'; diff --git a/src/main/resources/update/1_4.sql b/src/main/resources/update/1_4.sql deleted file mode 100644 index 2d3c492..0000000 --- a/src/main/resources/update/1_4.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TABLE GROUP_MEMBER( - GROUP_NAME VARCHAR(100) NOT NULL, - USER_NAME VARCHAR(100) NOT NULL -); - -ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME); -ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME); -ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME); - -ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE; - -CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS - SELECT - A.USER_NAME, - A.REPOSITORY_NAME, - A.ISSUE_ID, - NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT - FROM ISSUE A - LEFT OUTER JOIN ( - SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT - WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment') - GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID - ) B - ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID); diff --git a/src/main/resources/update/1_5.sql b/src/main/resources/update/1_5.sql deleted file mode 100644 index 03fc1bf..0000000 --- a/src/main/resources/update/1_5.sql +++ /dev/null @@ -1,21 +0,0 @@ -ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100); -ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100); -ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100); -ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100); - -CREATE TABLE PULL_REQUEST( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL, - BRANCH VARCHAR(100) NOT NULL, - REQUEST_USER_NAME VARCHAR(100) NOT NULL, - REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL, - REQUEST_BRANCH VARCHAR(100) NOT NULL, - COMMIT_ID_FROM VARCHAR(40) NOT NULL, - COMMIT_ID_TO VARCHAR(40) NOT NULL -); - -ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID); -ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); - -ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/src/main/resources/update/1_6.sql b/src/main/resources/update/1_6.sql deleted file mode 100644 index 43eb92d..0000000 --- a/src/main/resources/update/1_6.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE WEB_HOOK ( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - URL VARCHAR(200) NOT NULL -); - -ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL); -ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); diff --git a/src/main/resources/update/1_7.sql b/src/main/resources/update/1_7.sql deleted file mode 100644 index 9005ff9..0000000 --- a/src/main/resources/update/1_7.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE ACCOUNT ADD COLUMN FULL_NAME VARCHAR(100); - -UPDATE ACCOUNT SET FULL_NAME = USER_NAME WHERE FULL_NAME IS NULL; - -ALTER TABLE ACCOUNT ALTER COLUMN FULL_NAME SET NOT NULL; diff --git a/src/main/resources/update/1_8.sql b/src/main/resources/update/1_8.sql deleted file mode 100644 index 8b50ddf..0000000 --- a/src/main/resources/update/1_8.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/src/main/resources/update/2_3.sql b/src/main/resources/update/2_3.sql deleted file mode 100644 index 7620092..0000000 --- a/src/main/resources/update/2_3.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE PLUGIN ( - PLUGIN_ID VARCHAR(100) NOT NULL, - VERSION VARCHAR(100) NOT NULL -); - -ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID); diff --git a/src/main/resources/update/2_7.sql b/src/main/resources/update/2_7.sql deleted file mode 100644 index 6fa0684..0000000 --- a/src/main/resources/update/2_7.sql +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TABLE COMMIT_COMMENT ( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - COMMIT_ID VARCHAR(100) NOT NULL, - COMMENT_ID INT AUTO_INCREMENT, - COMMENTED_USER_NAME VARCHAR(100) NOT NULL, - CONTENT TEXT NOT NULL, - FILE_NAME NVARCHAR(100), - OLD_LINE_NUMBER INT, - NEW_LINE_NUMBER INT, - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL, - PULL_REQUEST BOOLEAN NOT NULL -); - -ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_PK PRIMARY KEY (COMMENT_ID); -ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); -ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, COMMENT_ID); diff --git a/src/main/resources/update/2_8.sql b/src/main/resources/update/2_8.sql deleted file mode 100644 index 38c95d3..0000000 --- a/src/main/resources/update/2_8.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE COMMIT_COMMENT ALTER COLUMN FILE_NAME NVARCHAR(260); diff --git a/src/main/resources/update/3_1.sql b/src/main/resources/update/3_1.sql deleted file mode 100644 index 3ddc48a..0000000 --- a/src/main/resources/update/3_1.sql +++ /dev/null @@ -1,42 +0,0 @@ -DROP TABLE IF EXISTS ACCESS_TOKEN; - -CREATE TABLE ACCESS_TOKEN ( - ACCESS_TOKEN_ID INT NOT NULL AUTO_INCREMENT, - TOKEN_HASH VARCHAR(40) NOT NULL, - USER_NAME VARCHAR(100) NOT NULL, - NOTE TEXT NOT NULL -); - -ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_PK PRIMARY KEY (ACCESS_TOKEN_ID); -ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME) - ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH); - - -DROP TABLE IF EXISTS COMMIT_STATUS; -CREATE TABLE COMMIT_STATUS( - COMMIT_STATUS_ID INT AUTO_INCREMENT, - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - COMMIT_ID VARCHAR(40) NOT NULL, - CONTEXT VARCHAR(255) NOT NULL, -- context is too long (maximum is 255 characters) - STATE VARCHAR(10) NOT NULL, -- pending, success, error, or failure - TARGET_URL VARCHAR(200), - DESCRIPTION TEXT, - CREATOR VARCHAR(100) NOT NULL, - REGISTERED_DATE TIMESTAMP NOT NULL, -- CREATED_AT - UPDATED_DATE TIMESTAMP NOT NULL -- UPDATED_AT -); -ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_PK PRIMARY KEY (COMMIT_STATUS_ID); -ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_1 - UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT); -ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK1 - FOREIGN KEY (USER_NAME, REPOSITORY_NAME) - REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME) - ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK2 - FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME) - ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK3 - FOREIGN KEY (CREATOR) REFERENCES ACCOUNT (USER_NAME) - ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/main/resources/update/3_11.sql b/src/main/resources/update/3_11.sql deleted file mode 100644 index 721690a..0000000 --- a/src/main/resources/update/3_11.sql +++ /dev/null @@ -1,25 +0,0 @@ -DROP TABLE IF EXISTS PROTECTED_BRANCH; - -CREATE TABLE PROTECTED_BRANCH( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - BRANCH VARCHAR(100) NOT NULL, - STATUS_CHECK_ADMIN BOOLEAN NOT NULL DEFAULT false -); - -ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH); -ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME) - ON DELETE CASCADE ON UPDATE CASCADE; - - -DROP TABLE IF EXISTS PROTECTED_BRANCH_REQUIRE_CONTEXT; -CREATE TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - BRANCH VARCHAR(100) NOT NULL, - CONTEXT VARCHAR(255) NOT NULL -); - -ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT); -ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, BRANCH) REFERENCES PROTECTED_BRANCH (USER_NAME, REPOSITORY_NAME, BRANCH) - ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/main/resources/update/3_13.sql b/src/main/resources/update/3_13.sql deleted file mode 100644 index 7dacf6b..0000000 --- a/src/main/resources/update/3_13.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100); diff --git a/src/main/resources/update/3_14.sql b/src/main/resources/update/3_14.sql deleted file mode 100644 index 8738dd3..0000000 --- a/src/main/resources/update/3_14.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE WEB_HOOK ADD COLUMN CTYPE VARCHAR(10); - -UPDATE WEB_HOOK SET CTYPE = 'form'; diff --git a/src/main/resources/update/3_9.sql b/src/main/resources/update/3_9.sql deleted file mode 100644 index b2f5c56..0000000 --- a/src/main/resources/update/3_9.sql +++ /dev/null @@ -1,55 +0,0 @@ -DROP TABLE IF EXISTS WEB_HOOK_EVENT; - -CREATE TABLE WEB_HOOK_EVENT( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - URL VARCHAR(200) NOT NULL, - EVENT VARCHAR(30) NOT NULL -); - -ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL, EVENT); -ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, URL) REFERENCES WEB_HOOK (USER_NAME, REPOSITORY_NAME, URL) - ON DELETE CASCADE ON UPDATE CASCADE; - -CREATE TEMPORARY TABLE TMP_EVENTS (EVENT VARCHAR(30)); - -INSERT INTO TMP_EVENTS VALUES ('push'),('issue_comment'),('issues'),('pull_request'); - -INSERT INTO WEB_HOOK_EVENT (USER_NAME, REPOSITORY_NAME, URL, EVENT) - SELECT USER_NAME, REPOSITORY_NAME, URL, EVENT - FROM WEB_HOOK, TMP_EVENTS; - -DROP TABLE TMP_EVENTS; - -ALTER TABLE COMMIT_COMMENT ADD COLUMN ISSUE_ID INT; - -CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS - SELECT - A.USER_NAME, - A.REPOSITORY_NAME, - A.ISSUE_ID, - NVL(B.COMMENT_COUNT, 0) + NVL(C.COMMENT_COUNT, 0) AS COMMENT_COUNT - FROM ISSUE A - LEFT OUTER JOIN ( - SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT - WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment') - GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID - ) B - ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID) - LEFT OUTER JOIN ( - SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT - GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID - ) C - ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID); - - -UPDATE COMMIT_COMMENT C SET (ISSUE_ID) = ( - SELECT MAX(P.ISSUE_ID) - FROM PULL_REQUEST P - WHERE - C.USER_NAME = P.USER_NAME AND - C.REPOSITORY_NAME = P.REPOSITORY_NAME AND - C.COMMIT_ID = P.COMMIT_ID_TO -); - -ALTER TABLE COMMIT_COMMENT DROP COLUMN PULL_REQUEST; \ No newline at end of file diff --git a/src/main/resources/update/gitbucket-core_4.0.sql b/src/main/resources/update/gitbucket-core_4.0.sql new file mode 100644 index 0000000..f97bdbc --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.0.sql @@ -0,0 +1,18 @@ +CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS + SELECT + A.USER_NAME, + A.REPOSITORY_NAME, + A.ISSUE_ID, + COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT + FROM ISSUE A + LEFT OUTER JOIN ( + SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT + WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment') + GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID + ) B + ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID) + LEFT OUTER JOIN ( + SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT + GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID + ) C + ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID); diff --git a/src/main/resources/update/gitbucket-core_4.0.xml b/src/main/resources/update/gitbucket-core_4.0.xml new file mode 100644 index 0000000..04f33fe --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.0.xml @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala new file mode 100644 index 0000000..3b85c72 --- /dev/null +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -0,0 +1,11 @@ +package gitbucket.core + +import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration} +import io.github.gitbucket.solidbase.model.{Version, Module} + +object GitBucketCoreModule extends Module("gitbucket-core", + new Version("4.0.0", + new LiquibaseMigration("update/gitbucket-core_4.0.xml"), + new SqlMigration("update/gitbucket-core_4.0.sql") + ) +) diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala index e74f9d4..f52491a 100644 --- a/src/main/scala/gitbucket/core/controller/FileUploadController.scala +++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala @@ -6,9 +6,11 @@ import gitbucket.core.util._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Directory._ +import gitbucket.core.util.Implicits._ import org.eclipse.jgit.api.Git import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.lib.{FileMode, Constants} +import org.scalatra import org.scalatra._ import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem} import org.apache.commons.io.{IOUtils, FileUtils} @@ -76,6 +78,21 @@ } getOrElse BadRequest } + post("/import") { + session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin => + execute({ (file, fileId) => + if(file.getName.endsWith(".xml")){ + import JDBCUtil._ + val conn = request2Session(request).conn + conn.importAsXML(file.getInputStream) + } else { + throw new RuntimeException("Import is available for only the XML file.") + } + }, _ => true) + } + redirect("/admin/data") + } + private def collaboratorsOnly(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = { implicit val session = Database.getSession(request) loginAccount match { diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 157a584..ed578d0 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -1,5 +1,7 @@ package gitbucket.core.controller +import java.io.FileInputStream + import gitbucket.core.admin.html import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService} import gitbucket.core.util.AdminAuthenticator @@ -11,7 +13,7 @@ import gitbucket.core.util.Directory._ import gitbucket.core.util.StringUtil._ import io.github.gitbucket.scalatra.forms._ -import org.apache.commons.io.FileUtils +import org.apache.commons.io.{IOUtils, FileUtils} import org.scalatra.i18n.Messages class SystemSettingsController extends SystemSettingsControllerBase @@ -74,6 +76,7 @@ case class PluginForm(pluginIds: List[String]) + case class DataExportForm(tableNames: List[String]) case class NewUserForm(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, @@ -268,6 +271,32 @@ } }) + get("/admin/data")(adminOnly { + import gitbucket.core.util.JDBCUtil._ + val session = request2Session(request) + html.data(session.conn.allTableNames()) + }) + + post("/admin/export")(adminOnly { + import gitbucket.core.util.JDBCUtil._ + val session = request2Session(request) + val file = if(params("type") == "sql"){ + session.conn.exportAsSQL(request.getParameterValues("tableNames").toSeq) + } else { + session.conn.exportAsXML(request.getParameterValues("tableNames").toSeq) + } + + contentType = "application/octet-stream" + response.setHeader("Content-Disposition", "attachment; filename=" + file.getName) + response.setContentLength(file.length.toInt) + + using(new FileInputStream(file)){ in => + IOUtils.copy(in, response.outputStream) + } + + () + }) + private def members: Constraint = new Constraint(){ override def validate(name: String, value: String, messages: Messages): Option[String] = { if(value.split(",").exists { diff --git a/src/main/scala/gitbucket/core/model/Plugin.scala b/src/main/scala/gitbucket/core/model/Plugin.scala deleted file mode 100644 index 1e8aac5..0000000 --- a/src/main/scala/gitbucket/core/model/Plugin.scala +++ /dev/null @@ -1,19 +0,0 @@ -package gitbucket.core.model - -trait PluginComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val Plugins = TableQuery[Plugins] - - class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){ - val pluginId = column[String]("PLUGIN_ID", O PrimaryKey) - val version = column[String]("VERSION") - def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply) - } -} - -case class Plugin( - pluginId: String, - version: String -) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index 0badf86..26bb225 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -1,5 +1,6 @@ package gitbucket.core.model +import gitbucket.core.util.DatabaseConfig trait Profile { val profile: slick.driver.JdbcProfile @@ -28,7 +29,9 @@ } trait ProfileProvider { self: Profile => - val profile = slick.driver.H2Driver + + lazy val profile = DatabaseConfig.slickDriver + } trait CoreProfile extends ProfileProvider with Profile @@ -49,7 +52,6 @@ with SshKeyComponent with WebHookComponent with WebHookEventComponent - with PluginComponent with ProtectedBranchComponent object Profile extends CoreProfile diff --git a/src/main/scala/gitbucket/core/plugin/Plugin.scala b/src/main/scala/gitbucket/core/plugin/Plugin.scala index 82c5867..caaf726 100644 --- a/src/main/scala/gitbucket/core/plugin/Plugin.scala +++ b/src/main/scala/gitbucket/core/plugin/Plugin.scala @@ -6,7 +6,7 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.util.ControlUtil._ -import gitbucket.core.util.Version +import io.github.gitbucket.solidbase.model.Version /** * Trait for define plugin interface. diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala index 12a4656..d784d87 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala @@ -10,9 +10,11 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.DatabaseConfig import gitbucket.core.util.Directory._ -import gitbucket.core.util.JDBCUtil._ -import gitbucket.core.util.{Version, Versions} +import io.github.gitbucket.solidbase.Solidbase +import io.github.gitbucket.solidbase.model.Module +import liquibase.database.core.H2Database import org.apache.commons.codec.binary.{Base64, StringUtils} import org.slf4j.LoggerFactory @@ -187,30 +189,15 @@ val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin] // Migration - val headVersion = plugin.versions.head - val currentVersion = conn.find("SELECT * FROM PLUGIN WHERE PLUGIN_ID = ?", plugin.pluginId)(_.getString("VERSION")) match { - case Some(x) => { - val dim = x.split("\\.") - Version(dim(0).toInt, dim(1).toInt) - } - case None => Version(0, 0) - } - - Versions.update(conn, headVersion, currentVersion, plugin.versions, new URLClassLoader(Array(pluginJar.toURI.toURL))){ conn => - currentVersion.versionString match { - case "0.0" => - conn.update("INSERT INTO PLUGIN (PLUGIN_ID, VERSION) VALUES (?, ?)", plugin.pluginId, headVersion.versionString) - case _ => - conn.update("UPDATE PLUGIN SET VERSION = ? WHERE PLUGIN_ID = ?", headVersion.versionString, plugin.pluginId) - } - } + val solidbase = new Solidbase() + solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*)) // Initialize plugin.initialize(instance, context, settings) instance.addPlugin(PluginInfo( pluginId = plugin.pluginId, pluginName = plugin.pluginName, - version = plugin.versions.head.versionString, + version = plugin.versions.head.getVersion, description = plugin.description, pluginClass = plugin )) diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 86430a9..529492d 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -108,25 +108,41 @@ } import gitbucket.core.model.Profile.commitStateColumnType val query = Q.query[Seq[(String, String, Int)], (String, String, Int, Int, Int, Option[String], Option[CommitState], Option[String], Option[String])](s""" - SELECT SUMM.USER_NAME, SUMM.REPOSITORY_NAME, SUMM.ISSUE_ID, CS_ALL, CS_SUCCESS - , CSD.CONTEXT, CSD.STATE, CSD.TARGET_URL, CSD.DESCRIPTION - FROM (SELECT - PR.USER_NAME - , PR.REPOSITORY_NAME - , PR.ISSUE_ID - , COUNT(CS.STATE) AS CS_ALL - , SUM(CS.STATE='success') AS CS_SUCCESS - , PR.COMMIT_ID_TO AS COMMIT_ID + SELECT + SUMM.USER_NAME, + SUMM.REPOSITORY_NAME, + SUMM.ISSUE_ID, + CS_ALL, + CS_SUCCESS, + CSD.CONTEXT, + CSD.STATE, + CSD.TARGET_URL, + CSD.DESCRIPTION + FROM ( + SELECT + PR.USER_NAME, + PR.REPOSITORY_NAME, + PR.ISSUE_ID, + COUNT(CS.STATE) AS CS_ALL, + CSS.CS_SUCCESS AS CS_SUCCESS, + PR.COMMIT_ID_TO AS COMMIT_ID FROM PULL_REQUEST PR JOIN COMMIT_STATUS CS - ON PR.USER_NAME=CS.USER_NAME - AND PR.REPOSITORY_NAME=CS.REPOSITORY_NAME - AND PR.COMMIT_ID_TO=CS.COMMIT_ID - WHERE $issueIdQuery - GROUP BY PR.USER_NAME, PR.REPOSITORY_NAME, PR.ISSUE_ID) as SUMM + ON PR.USER_NAME = CS.USER_NAME AND PR.REPOSITORY_NAME = CS.REPOSITORY_NAME AND PR.COMMIT_ID_TO = CS.COMMIT_ID + JOIN ( + SELECT + COUNT(*) AS CS_SUCCESS, + USER_NAME, + REPOSITORY_NAME, + COMMIT_ID + FROM COMMIT_STATUS WHERE STATE = 'success' GROUP BY USER_NAME, REPOSITORY_NAME, COMMIT_ID + ) CSS ON PR.USER_NAME = CSS.USER_NAME AND PR.REPOSITORY_NAME = CSS.REPOSITORY_NAME AND PR.COMMIT_ID_TO = CSS.COMMIT_ID + WHERE $issueIdQuery + GROUP BY PR.USER_NAME, PR.REPOSITORY_NAME, PR.ISSUE_ID, CSS.CS_SUCCESS + ) as SUMM LEFT OUTER JOIN COMMIT_STATUS CSD ON SUMM.CS_ALL = 1 AND SUMM.COMMIT_ID = CSD.COMMIT_ID"""); - query(issueList).list.map{ + query(issueList).list.map { case(userName, repositoryName, issueId, count, successCount, context, state, targetUrl, description) => (userName, repositoryName, issueId) -> CommitStatusInfo(count, successCount, context, state, targetUrl, description) }.toMap diff --git a/src/main/scala/gitbucket/core/service/PluginService.scala b/src/main/scala/gitbucket/core/service/PluginService.scala deleted file mode 100644 index 99a20d8..0000000 --- a/src/main/scala/gitbucket/core/service/PluginService.scala +++ /dev/null @@ -1,24 +0,0 @@ -package gitbucket.core.service - -import gitbucket.core.model.Plugin -import gitbucket.core.model.Profile._ -import profile.simple._ - -trait PluginService { - - def getPlugins()(implicit s: Session): List[Plugin] = - Plugins.sortBy(_.pluginId).list - - def registerPlugin(plugin: Plugin)(implicit s: Session): Unit = - Plugins.insert(plugin) - - def updatePlugin(plugin: Plugin)(implicit s: Session): Unit = - Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version) - - def deletePlugin(pluginId: String)(implicit s: Session): Unit = - Plugins.filter(_.pluginId === pluginId.bind).delete - - def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] = - Plugins.filter(_.pluginId === pluginId.bind).firstOption - -} diff --git a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala b/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala deleted file mode 100644 index 22b05dd..0000000 --- a/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala +++ /dev/null @@ -1,187 +0,0 @@ -package gitbucket.core.servlet - -import java.io.File -import java.sql.{DriverManager, Connection} -import gitbucket.core.plugin.PluginRegistry -import gitbucket.core.service.SystemSettingsService -import gitbucket.core.util._ -import org.apache.commons.io.FileUtils -import javax.servlet.{ServletContextListener, ServletContextEvent} -import org.slf4j.LoggerFactory -import Directory._ -import ControlUtil._ -import JDBCUtil._ -import org.eclipse.jgit.api.Git -import gitbucket.core.util.Versions -import gitbucket.core.util.Directory - -object AutoUpdate { - - /** - * The history of versions. A head of this sequence is the current GitBucket version. - */ - val versions = Seq( - new Version(3, 14), - new Version(3, 13), - new Version(3, 12), - new Version(3, 11), - new Version(3, 10), - new Version(3, 9), - new Version(3, 8), - new Version(3, 7) with SystemSettingsService { - override def update(conn: Connection, cl: ClassLoader): Unit = { - super.update(conn, cl) - val settings = loadSystemSettings() - if(settings.notification){ - saveSystemSettings(settings.copy(useSMTP = true)) - } - } - }, - new Version(3, 6), - new Version(3, 5), - new Version(3, 4), - new Version(3, 3), - new Version(3, 2), - new Version(3, 1), - new Version(3, 0), - new Version(2, 8), - new Version(2, 7) { - override def update(conn: Connection, cl: ClassLoader): Unit = { - super.update(conn, cl) - conn.select("SELECT * FROM REPOSITORY"){ rs => - // Rename attached files directory from /issues to /comments - val userName = rs.getString("USER_NAME") - val repoName = rs.getString("REPOSITORY_NAME") - defining(Directory.getAttachedDir(userName, repoName)){ newDir => - val oldDir = new File(newDir.getParentFile, "issues") - if(oldDir.exists && oldDir.isDirectory){ - oldDir.renameTo(newDir) - } - } - // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist - val originalUserName = rs.getString("ORIGIN_USER_NAME") - val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME") - if(originalUserName != null && originalRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - originalUserName, originalRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist - val parentUserName = rs.getString("PARENT_USER_NAME") - val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME") - if(parentUserName != null && parentRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - parentUserName, parentRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - } - } - }, - new Version(2, 6), - new Version(2, 5), - new Version(2, 4), - new Version(2, 3) { - override def update(conn: Connection, cl: ClassLoader): Unit = { - super.update(conn, cl) - conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs => - val curInfo = rs.getString("ADDITIONAL_INFO") - val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n") - if (curInfo != newInfo) { - conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID")) - } - } - ignore { - FileUtils.deleteDirectory(Directory.getPluginCacheDir()) - //FileUtils.deleteDirectory(new File(Directory.PluginHome)) - } - } - }, - new Version(2, 2), - new Version(2, 1), - new Version(2, 0){ - override def update(conn: Connection, cl: ClassLoader): Unit = { - import eu.medsea.mimeutil.{MimeUtil2, MimeType} - - val mimeUtil = new MimeUtil2() - mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") - - super.update(conn, cl) - conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => - defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => - if(dir.exists && dir.isDirectory){ - dir.listFiles.foreach { file => - if(file.getName.indexOf('.') < 0){ - val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString - if(mimeType.startsWith("image/")){ - file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) - } - } - } - } - } - } - } - }, - Version(1, 13), - Version(1, 12), - Version(1, 11), - Version(1, 10), - Version(1, 9), - Version(1, 8), - Version(1, 7), - Version(1, 6), - Version(1, 5), - Version(1, 4), - new Version(1, 3){ - override def update(conn: Connection, cl: ClassLoader): Unit = { - super.update(conn, cl) - // Fix wiki repository configuration - conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => - using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => - defining(git.getRepository.getConfig){ config => - if(!config.getBoolean("http", "receivepack", false)){ - config.setBoolean("http", null, "receivepack", true) - config.save - } - } - } - } - } - }, - Version(1, 2), - Version(1, 1), - Version(1, 0), - Version(0, 0) - ) - - /** - * The head version of GitBucket. - */ - val headVersion = versions.head - - /** - * The version file (GITBUCKET_HOME/version). - */ - lazy val versionFile = new File(GitBucketHome, "version") - - /** - * Returns the current version from the version file. - */ - def getCurrentVersion(): Version = { - if(versionFile.exists){ - FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { - case Array(majorVersion, minorVersion) => { - versions.find { v => - v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt - }.getOrElse(Version(0, 0)) - } - case _ => Version(0, 0) - } - } else Version(0, 0) - } - -} diff --git a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala index a375b37..f494a4a 100644 --- a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala +++ b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala @@ -1,16 +1,22 @@ package gitbucket.core.servlet +import java.io.File + import akka.event.Logging import com.typesafe.config.ConfigFactory +import gitbucket.core.GitBucketCoreModule import gitbucket.core.plugin.PluginRegistry import gitbucket.core.service.{ActivityService, SystemSettingsService} -import org.apache.commons.io.FileUtils +import gitbucket.core.util.DatabaseConfig +import gitbucket.core.util.Directory._ +import gitbucket.core.util.JDBCUtil._ +import io.github.gitbucket.solidbase.Solidbase +import io.github.gitbucket.solidbase.manager.JDBCVersionManager import javax.servlet.{ServletContextListener, ServletContextEvent} +import org.apache.commons.io.FileUtils import org.slf4j.LoggerFactory -import gitbucket.core.util.Versions import akka.actor.{Actor, Props, ActorSystem} import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension -import AutoUpdate._ /** * Initialize GitBucket system. @@ -30,14 +36,49 @@ Database() withTransaction { session => val conn = session.conn - // Migration - logger.debug("Start schema update") - Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn => - FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") + // Check version + val versionFile = new File(GitBucketHome, "version") + + if(versionFile.exists()){ + val version = FileUtils.readFileToString(versionFile, "UTF-8") + if(version == "3.14"){ + // Initialization for GitBucket 3.14 + logger.info("Migration to GitBucket 4.x start") + + // Backup current data + val dataMvFile = new File(GitBucketHome, "data.mv.db") + if(dataMvFile.exists) { + FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14")) + } + val dataTraceFile = new File(GitBucketHome, "data.trace.db") + if(dataTraceFile.exists) { + FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14")) + } + + // Change form + val manager = new JDBCVersionManager(conn) + manager.initialize() + manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0") + conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs => + manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION")) + } + conn.update("DROP TABLE PLUGIN") + versionFile.delete() + + logger.info("Migration to GitBucket 4.x completed") + + } else { + throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.") + } } + // Run normal migration + logger.info("Start schema update") + val solidbase = new Solidbase() + solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule) + // Load plugins - logger.debug("Initialize plugins") + logger.info("Initialize plugins") PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn) } diff --git a/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala b/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala index fc2e457..bad06e1 100644 --- a/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala +++ b/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala @@ -2,7 +2,7 @@ import javax.servlet._ import javax.servlet.http.HttpServletRequest -import com.mchange.v2.c3p0.ComboPooledDataSource +import com.zaxxer.hikari._ import gitbucket.core.util.DatabaseConfig import org.scalatra.ScalatraBase import org.slf4j.LoggerFactory @@ -46,14 +46,14 @@ private val logger = LoggerFactory.getLogger(Database.getClass) - private val dataSource: ComboPooledDataSource = { - val ds = new ComboPooledDataSource - ds.setDriverClass(DatabaseConfig.driver) - ds.setJdbcUrl(DatabaseConfig.url) - ds.setUser(DatabaseConfig.user) - ds.setPassword(DatabaseConfig.password) + private val dataSource: HikariDataSource = { + val config = new HikariConfig() + config.setDriverClassName(DatabaseConfig.jdbcDriver) + config.setJdbcUrl(DatabaseConfig.url) + config.setUsername(DatabaseConfig.user) + config.setPassword(DatabaseConfig.password) logger.debug("load database connection pool") - ds + new HikariDataSource(config) } private val db: SlickDatabase = { diff --git a/src/main/scala/gitbucket/core/util/DatabaseConfig.scala b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala index ffe7cee..ec68d0b 100644 --- a/src/main/scala/gitbucket/core/util/DatabaseConfig.scala +++ b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala @@ -1,19 +1,83 @@ package gitbucket.core.util import com.typesafe.config.ConfigFactory -import Directory.DatabaseHome +import java.io.File +import Directory._ +import liquibase.database.AbstractJdbcDatabase +import liquibase.database.core.{PostgresDatabase, MySQLDatabase, H2Database} +import org.apache.commons.io.FileUtils object DatabaseConfig { - private val config = ConfigFactory.load("database") - private val dbUrl = config.getString("db.url") + private lazy val config = { + val file = new File(GitBucketHome, "database.conf") + if(!file.exists){ + FileUtils.write(file, + """db { + | url = "jdbc:h2:${DatabaseHome};MVCC=true" + | user = "sa" + | password = "sa" + |} + |""".stripMargin, "UTF-8") + } + ConfigFactory.parseFile(file) + } + + private lazy val dbUrl = config.getString("db.url") def url(directory: Option[String]): String = dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome)) - val url: String = url(None) - val user: String = config.getString("db.user") - val password: String = config.getString("db.password") - val driver: String = config.getString("db.driver") + lazy val url: String = url(None) + lazy val user: String = config.getString("db.user") + lazy val password: String = config.getString("db.password") + lazy val jdbcDriver: String = DatabaseType(url).jdbcDriver + lazy val slickDriver: slick.driver.JdbcProfile = DatabaseType(url).slickDriver + lazy val liquiDriver: AbstractJdbcDatabase = DatabaseType(url).liquiDriver } + +sealed trait DatabaseType { + val jdbcDriver: String + val slickDriver: slick.driver.JdbcProfile + val liquiDriver: AbstractJdbcDatabase +} + +object DatabaseType { + + def apply(url: String): DatabaseType = { + if(url.startsWith("jdbc:h2:")){ + H2 + } else if(url.startsWith("jdbc:mysql:")){ + MySQL + } else if(url.startsWith("jdbc:postgresql:")){ + PostgreSQL + } else { + throw new IllegalArgumentException(s"${url} is not supported.") + } + } + + object H2 extends DatabaseType { + val jdbcDriver = "org.h2.Driver" + val slickDriver = slick.driver.H2Driver + val liquiDriver = new H2Database() + } + + object MySQL extends DatabaseType { + val jdbcDriver = "com.mysql.jdbc.Driver" + val slickDriver = slick.driver.MySQLDriver + val liquiDriver = new MySQLDatabase() + } + + object PostgreSQL extends DatabaseType { + val jdbcDriver = "org.postgresql.Driver2" + val slickDriver = new slick.driver.PostgresDriver { + override def quoteIdentifier(id: String): String = { + val s = new StringBuilder(id.length + 4) append '"' + for(c <- id) if(c == '"') s append "\"\"" else s append c.toLower + (s append '"').toString + } + } + val liquiDriver = new PostgresDatabase() + } +} diff --git a/src/main/scala/gitbucket/core/util/JDBCUtil.scala b/src/main/scala/gitbucket/core/util/JDBCUtil.scala index 41a2b1a..60116a5 100644 --- a/src/main/scala/gitbucket/core/util/JDBCUtil.scala +++ b/src/main/scala/gitbucket/core/util/JDBCUtil.scala @@ -1,7 +1,13 @@ package gitbucket.core.util +import java.io._ import java.sql._ +import java.text.SimpleDateFormat +import javax.xml.stream.{XMLStreamConstants, XMLInputFactory, XMLOutputFactory} import ControlUtil._ +import scala.StringBuilder +import scala.annotation.tailrec +import scala.collection.mutable import scala.collection.mutable.ListBuffer /** @@ -58,6 +64,265 @@ } } + def importAsXML(in: InputStream): Unit = { + conn.setAutoCommit(false) + try { + val factory = XMLInputFactory.newInstance() + using(factory.createXMLStreamReader(in, "UTF-8")){ reader => + // stateful objects + var elementName = "" + var insertTable = "" + var insertColumns = Map.empty[String, (String, String)] + + while(reader.hasNext){ + reader.next() + + reader.getEventType match { + case XMLStreamConstants.START_ELEMENT => + elementName = reader.getName.getLocalPart + if(elementName == "insert"){ + insertTable = reader.getAttributeValue(null, "table") + } else if(elementName == "delete"){ + val tableName = reader.getAttributeValue(null, "table") + conn.update(s"DELETE FROM ${tableName}") + } else if(elementName == "column"){ + val columnName = reader.getAttributeValue(null, "name") + val columnType = reader.getAttributeValue(null, "type") + val columnValue = reader.getElementText + insertColumns = insertColumns + (columnName -> (columnType, columnValue)) + } + case XMLStreamConstants.END_ELEMENT => + // Execute insert statement + reader.getName.getLocalPart match { + case "insert" => { + val sb = new StringBuilder() + sb.append(s"INSERT INTO ${insertTable} (") + sb.append(insertColumns.map { case (columnName, _) => columnName }.mkString(", ")) + sb.append(") VALUES (") + sb.append(insertColumns.map { case (_, (columnType, columnValue)) => + if(columnType == null || columnValue == null){ + "NULL" + } else if(columnType == "string"){ + "'" + columnValue.replace("'", "''") + "'" + } else if(columnType == "timestamp"){ + "'" + columnValue + "'" + } else { + columnValue.toString + } + }.mkString(", ")) + sb.append(")") + + conn.update(sb.toString) + + insertColumns = Map.empty[String, (String, String)] // Clear column information + } + case _ => // Nothing to do + } + case _ => // Nothing to do + } + } + } + + conn.commit() + + } catch { + case e: Exception => { + conn.rollback() + throw e + } + } + } + + def exportAsXML(targetTables: Seq[String]): File = { + val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") + val file = File.createTempFile("gitbucket-export-", ".xml") + + val factory = XMLOutputFactory.newInstance() + using(factory.createXMLStreamWriter(new FileOutputStream(file), "UTF-8")){ writer => + val dbMeta = conn.getMetaData + val allTablesInDatabase = allTablesOrderByDependencies(dbMeta) + + writer.writeStartDocument("UTF-8", "1.0") + writer.writeStartElement("tables") + + println(allTablesInDatabase.mkString(", ")) + + allTablesInDatabase.reverse.foreach { tableName => + if (targetTables.contains(tableName)) { + writer.writeStartElement("delete") + writer.writeAttribute("table", tableName) + writer.writeEndElement() + } + } + + allTablesInDatabase.foreach { tableName => + if (targetTables.contains(tableName)) { + select(s"SELECT * FROM ${tableName}") { rs => + writer.writeStartElement("insert") + writer.writeAttribute("table", tableName) + val rsMeta = rs.getMetaData + (1 to rsMeta.getColumnCount).foreach { i => + val columnName = rsMeta.getColumnName(i) + val (columnType, columnValue) = if(rs.getObject(columnName) == null){ + (null, null) + } else { + rsMeta.getColumnType(i) match { + case Types.BOOLEAN | Types.BIT => ("boolean", rs.getBoolean(columnName)) + case Types.VARCHAR | Types.CLOB | Types.CHAR | Types.LONGVARCHAR => ("string", rs.getString(columnName)) + case Types.INTEGER => ("int", rs.getInt(columnName)) + case Types.TIMESTAMP => ("timestamp", dateFormat.format(rs.getTimestamp(columnName))) + } + } + writer.writeStartElement("column") + writer.writeAttribute("name", columnName) + if(columnType != null){ + writer.writeAttribute("type", columnType) + } + if(columnValue != null){ + writer.writeCharacters(columnValue.toString) + } + writer.writeEndElement() + } + writer.writeEndElement() + } + } + } + + writer.writeEndElement() + writer.writeEndDocument() + } + + file + } + + def exportAsSQL(targetTables: Seq[String]): File = { + val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") + val file = File.createTempFile("gitbucket-export-", ".sql") + + using(new FileOutputStream(file)) { out => + val dbMeta = conn.getMetaData + val allTablesInDatabase = allTablesOrderByDependencies(dbMeta) + + allTablesInDatabase.reverse.foreach { tableName => + if (targetTables.contains(tableName)) { + out.write(s"DELETE FROM ${tableName};\n".getBytes("UTF-8")) + } + } + + allTablesInDatabase.foreach { tableName => + if (targetTables.contains(tableName)) { + val sb = new StringBuilder() + select(s"SELECT * FROM ${tableName}") { rs => + sb.append(s"INSERT INTO ${tableName} (") + + val rsMeta = rs.getMetaData + val columns = (1 to rsMeta.getColumnCount).map { i => + (rsMeta.getColumnName(i), rsMeta.getColumnType(i)) + } + sb.append(columns.map(_._1).mkString(", ")) + sb.append(") VALUES (") + + val values = columns.map { case (columnName, columnType) => + if(rs.getObject(columnName) == null){ + null + } else { + columnType match { + case Types.BOOLEAN | Types.BIT => rs.getBoolean(columnName) + case Types.VARCHAR | Types.CLOB | Types.CHAR | Types.LONGVARCHAR => rs.getString(columnName) + case Types.INTEGER => rs.getInt(columnName) + case Types.TIMESTAMP => rs.getTimestamp(columnName) + } + } + } + + val columnValues = values.map { value => + value match { + case x: String => "'" + x.replace("'", "''") + "'" + case x: Timestamp => "'" + dateFormat.format(x) + "'" + case null => "NULL" + case x => x + } + } + sb.append(columnValues.mkString(", ")) + sb.append(");\n") + } + + out.write(sb.toString.getBytes("UTF-8")) + } + } + } + + file + } + + def allTableNames(): Seq[String] = { + using(conn.getMetaData.getTables(null, null, "%", Seq("TABLE").toArray)) { rs => + val tableNames = new ListBuffer[String] + while (rs.next) { + val name = rs.getString("TABLE_NAME").toUpperCase + if (name != "VERSIONS" && name != "PLUGIN") { + tableNames += name + } + } + tableNames.toSeq + } + } + + private def childTables(meta: DatabaseMetaData, tableName: String): Seq[String] = { + val normalizedTableName = + if(meta.getDatabaseProductName == "PostgreSQL"){ + tableName.toLowerCase + } else { + tableName + } + + using(meta.getExportedKeys(null, null, normalizedTableName)) { rs => + val children = new ListBuffer[String] + while (rs.next) { + val childTableName = rs.getString("FKTABLE_NAME").toUpperCase + if(!children.contains(childTableName)){ + children += childTableName + children ++= childTables(meta, childTableName) + } + } + children.distinct.toSeq + } + } + + + private def allTablesOrderByDependencies(meta: DatabaseMetaData): Seq[String] = { + val tables = allTableNames.map { tableName => + val result = TableDependency(tableName, childTables(meta, tableName)) + result + } + + val edges = tables.flatMap { table => + table.children.map { child => (table.tableName, child) } + } + + tsort(edges).toSeq + } + + case class TableDependency(tableName: String, children: Seq[String]) + + + def tsort[A](edges: Traversable[(A, A)]): Iterable[A] = { + @tailrec + def tsort(toPreds: Map[A, Set[A]], done: Iterable[A]): Iterable[A] = { + val (noPreds, hasPreds) = toPreds.partition { _._2.isEmpty } + if (noPreds.isEmpty) { + if (hasPreds.isEmpty) done else sys.error(hasPreds.toString) + } else { + val found = noPreds.map { _._1 } + tsort(hasPreds.mapValues { _ -- found }, done ++ found) + } + } + + val toPred = edges.foldLeft(Map[A, Set[A]]()) { (acc, e) => + acc + (e._1 -> acc.getOrElse(e._1, Set())) + (e._2 -> (acc.getOrElse(e._2, Set()) + e._1)) + } + tsort(toPred, Seq()) + } } } diff --git a/src/main/scala/gitbucket/core/util/Version.scala b/src/main/scala/gitbucket/core/util/Version.scala deleted file mode 100644 index 3c7ffe6..0000000 --- a/src/main/scala/gitbucket/core/util/Version.scala +++ /dev/null @@ -1,67 +0,0 @@ -package gitbucket.core.util - -import java.sql.Connection - -import org.apache.commons.io.IOUtils -import org.slf4j.LoggerFactory -import ControlUtil._ - -case class Version(majorVersion: Int, minorVersion: Int) { - - private val logger = LoggerFactory.getLogger(classOf[Version]) - - /** - * 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, cl: ClassLoader): Unit = { - val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" - - using(cl.getResourceAsStream(sqlPath)){ in => - if(in != null){ - val sql = IOUtils.toString(in, "UTF-8") - using(conn.createStatement()){ stmt => - logger.debug(sqlPath + "=" + sql) - stmt.executeUpdate(sql) - } - } - } - } - - - /** - * MAJOR.MINOR - */ - val versionString = s"${majorVersion}.${minorVersion}" - -} - -object Versions { - - private val logger = LoggerFactory.getLogger(Versions.getClass) - - def update(conn: Connection, headVersion: Version, currentVersion: Version, versions: Seq[Version], cl: ClassLoader) - (save: Connection => Unit): Unit = { - logger.debug("Start schema update") - try { - if(currentVersion == headVersion){ - logger.debug("No update") - } else if(currentVersion.versionString != "0.0" && !versions.contains(currentVersion)){ - logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") - } else { - versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn, cl)) - save(conn) - logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") - } - } catch { - case ex: Throwable => { - logger.error("Failed to schema update", ex) - ex.printStackTrace() - conn.rollback() - } - } - logger.debug("End schema update") - } - -} - diff --git a/src/main/twirl/gitbucket/core/admin/data.scala.html b/src/main/twirl/gitbucket/core/admin/data.scala.html new file mode 100644 index 0000000..669d75d --- /dev/null +++ b/src/main/twirl/gitbucket/core/admin/data.scala.html @@ -0,0 +1,56 @@ +@(tableNames: Seq[String])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Data export / import"){ + @admin.html.menu("data") { +
+
Export
+
+
+ @tableNames.map { tableName => +
+ +
+ } + +
+ +
+
+ +
+
+
+
+ +
+
Import (only XML)
+
+
+ + +
+
+
+ } +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/admin/menu.scala.html b/src/main/twirl/gitbucket/core/admin/menu.scala.html index edc7d7b..fedc73c 100644 --- a/src/main/twirl/gitbucket/core/admin/menu.scala.html +++ b/src/main/twirl/gitbucket/core/admin/menu.scala.html @@ -12,6 +12,9 @@ Plugins + + Data export / import +
  • H2 Console
  • diff --git a/src/main/twirl/gitbucket/core/admin/system.scala.html b/src/main/twirl/gitbucket/core/admin/system.scala.html index 5139e70..7001150 100644 --- a/src/main/twirl/gitbucket/core/admin/system.scala.html +++ b/src/main/twirl/gitbucket/core/admin/system.scala.html @@ -9,11 +9,23 @@
    System Settings
    - - - - - @GitBucketHome + + + + + + + + + + + + + + + + +
    PropertyValue
    GITBUCKET_HOME@GitBucketHome
    DATBASE_URL@gitbucket.core.util.DatabaseConfig.url
    diff --git a/src/main/twirl/gitbucket/core/main.scala.html b/src/main/twirl/gitbucket/core/main.scala.html index fa30715..c595945 100644 --- a/src/main/twirl/gitbucket/core/main.scala.html +++ b/src/main/twirl/gitbucket/core/main.scala.html @@ -1,6 +1,5 @@ @(title: String, repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.plugin.PluginRegistry -@import gitbucket.core.servlet.AutoUpdate @import context._ @import gitbucket.core.view.helpers._ @@ -51,9 +50,7 @@ *@ GitBucket - @defining(AutoUpdate.getCurrentVersion){ version => - @version.majorVersion.@version.minorVersion - } + @gitbucket.core.GitBucketCoreModule.getVersions.get(0).getVersion @if(loginAccount.isDefined){ @repository.map { repository => diff --git a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala index 06aaaf7..87eb29a 100644 --- a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala +++ b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala @@ -1,11 +1,16 @@ package gitbucket.core.service -import gitbucket.core.servlet.AutoUpdate +import gitbucket.core.GitBucketCoreModule import gitbucket.core.util.{ControlUtil, DatabaseConfig, FileUtil} import gitbucket.core.util.ControlUtil._ import gitbucket.core.model._ import gitbucket.core.model.Profile._ +import io.github.gitbucket.solidbase.Solidbase +import liquibase.database.core.H2Database +import liquibase.database.jvm.JdbcConnection import profile.simple._ +import scalaz._ +import Scalaz._ import org.apache.commons.io.FileUtils @@ -22,7 +27,9 @@ val (url, user, pass) = (DatabaseConfig.url(Some(dir.toString)), DatabaseConfig.user, DatabaseConfig.password) org.h2.Driver.load() using(DriverManager.getConnection(url, user, pass)){ conn => - AutoUpdate.versions.reverse.foreach(_.update(conn, Thread.currentThread.getContextClassLoader)) + val solidbase = new Solidbase() + val db = new H2Database() <| { _.setConnection(new JdbcConnection(conn)) } // TODO Remove setConnection in the future + solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, db, GitBucketCoreModule) } Database.forURL(url, user, pass).withSession { session => action(session)