diff --git a/etc/deploy-assemby-jar.sh b/etc/deploy-assemby-jar.sh
new file mode 100644
index 0000000..11f276d
--- /dev/null
+++ b/etc/deploy-assemby-jar.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+mvn deploy:deploy-file \
+ -DgroupId=jp.sf.amateras\
+ -DartifactId=gitbucket-assembly\
+ -Dversion=0.0.1\
+ -Dpackaging=jar\
+ -Dfile=../target/scala-2.11/gitbucket-assembly-0.0.1.jar\
+ -DrepositoryId=sourceforge.jp\
+ -Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
\ No newline at end of file
diff --git a/etc/pom.xml b/etc/pom.xml
new file mode 100644
index 0000000..40693f2
--- /dev/null
+++ b/etc/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+ jp.sf.amateras
+ gitbucket-assembly
+ 0.0.1
+
+
+
+ org.apache.maven.wagon
+ wagon-ssh
+ 1.0-beta-6
+
+
+
+
\ No newline at end of file
diff --git a/project/build.scala b/project/build.scala
index e767078..7d41538 100644
--- a/project/build.scala
+++ b/project/build.scala
@@ -4,6 +4,8 @@
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
+import sbtassembly._
+import sbtassembly.AssemblyKeys._
object MyBuild extends Build {
val Organization = "jp.sf.amateras"
@@ -18,6 +20,17 @@
)
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
.settings(
+ test in assembly := {},
+ assemblyMergeStrategy in assembly := {
+ case PathList("META-INF", xs @ _*) =>
+ (xs map {_.toLowerCase}) match {
+ case ("manifest.mf" :: Nil) => MergeStrategy.discard
+ case _ => MergeStrategy.discard
+ }
+ case x => MergeStrategy.first
+ }
+ )
+ .settings(
sourcesInBase := false,
organization := Organization,
name := Name,
@@ -45,7 +58,7 @@
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.180",
- "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
+// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test",
diff --git a/project/plugins.sbt b/project/plugins.sbt
index de95ab6..dee3cdf 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -7,3 +7,5 @@
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
+
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
\ No newline at end of file
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index bdbd898..cfa37e6 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -1,5 +1,7 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._
+import plugin.PluginRegistry
+
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
@@ -15,6 +17,11 @@
// Register controllers
context.mount(new AnonymousAccessController, "/*")
+
+ PluginRegistry().getControllers.foreach { case (controller, path) =>
+ context.mount(controller, path)
+ }
+
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
diff --git a/src/main/scala/plugin/Plugin.scala b/src/main/scala/plugin/Plugin.scala
new file mode 100644
index 0000000..c176874
--- /dev/null
+++ b/src/main/scala/plugin/Plugin.scala
@@ -0,0 +1,30 @@
+package plugin
+
+import javax.servlet.ServletContext
+
+import util.Version
+
+/**
+ * Trait for define plugin interface.
+ * To provide plugin, put Plugin class which mixed in this trait into the package root.
+ */
+trait Plugin {
+
+ val pluginId: String
+ val pluginName: String
+ val description: String
+ val versions: Seq[Version]
+
+ /**
+ * This method is invoked in initialization of plugin system.
+ * Register plugin functionality to PluginRegistry.
+ */
+ def initialize(registry: PluginRegistry): Unit
+
+ /**
+ * This method is invoked in shutdown of plugin system.
+ * If the plugin has any resources, release them in this method.
+ */
+ def shutdown(registry: PluginRegistry): Unit
+
+}
diff --git a/src/main/scala/plugin/PluginRegistory.scala b/src/main/scala/plugin/PluginRegistory.scala
new file mode 100644
index 0000000..bb7c2cb
--- /dev/null
+++ b/src/main/scala/plugin/PluginRegistory.scala
@@ -0,0 +1,145 @@
+package plugin
+
+import java.io.{FilenameFilter, File}
+import java.net.URLClassLoader
+import javax.servlet.ServletContext
+import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
+
+import org.slf4j.LoggerFactory
+import service.RepositoryService.RepositoryInfo
+import util.Directory._
+import util.JDBCUtil._
+import util.{Version, Versions}
+
+import scala.collection.mutable.ListBuffer
+import app.{ControllerBase, Context}
+
+class PluginRegistry {
+
+ private val plugins = new ListBuffer[PluginInfo]
+ private val javaScripts = new ListBuffer[(String, String)]
+ private val controllers = new ListBuffer[(ControllerBase, String)]
+
+ def addPlugin(pluginInfo: PluginInfo): Unit = {
+ plugins += pluginInfo
+ }
+
+ def getPlugins(): List[PluginInfo] = plugins.toList
+
+ def addController(controller: ControllerBase, path: String): Unit = {
+ controllers += ((controller, path))
+ }
+
+ def getControllers(): List[(ControllerBase, String)] = controllers.toList
+
+ def addJavaScript(path: String, script: String): Unit = {
+ javaScripts += Tuple2(path, script)
+ }
+
+ //def getJavaScripts(): List[(String, String)] = javaScripts.toList
+
+ def getJavaScript(currentPath: String): Option[String] = {
+ javaScripts.find(x => currentPath.matches(x._1)).map(_._2)
+ }
+
+ private case class GlobalAction(
+ method: String,
+ path: String,
+ function: (HttpServletRequest, HttpServletResponse, Context) => Any
+ )
+
+ private case class RepositoryAction(
+ method: String,
+ path: String,
+ function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any
+ )
+
+}
+
+/**
+ * Provides entry point to PluginRegistry.
+ */
+object PluginRegistry {
+
+ private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
+
+ private val instance = new PluginRegistry()
+
+ /**
+ * Returns the PluginRegistry singleton instance.
+ */
+ def apply(): PluginRegistry = instance
+
+ /**
+ * Initializes all installed plugins.
+ */
+ def initialize(context: ServletContext, conn: java.sql.Connection): Unit = {
+ val pluginDir = new File(PluginHome)
+ if(pluginDir.exists && pluginDir.isDirectory){
+ pluginDir.listFiles(new FilenameFilter {
+ override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
+ }).foreach { pluginJar =>
+ val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
+ try {
+ 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)
+ }
+ }
+
+ // Initialize
+ plugin.initialize(instance)
+ instance.addPlugin(PluginInfo(
+ pluginId = plugin.pluginId,
+ pluginName = plugin.pluginName,
+ version = plugin.versions.head.versionString,
+ description = plugin.description,
+ pluginClass = plugin
+ ))
+
+ } catch {
+ case e: Exception => {
+ logger.error(s"Error during plugin initialization", e)
+ }
+ }
+ }
+ }
+ }
+
+ def shutdown(context: ServletContext): Unit = {
+ instance.getPlugins().foreach { pluginInfo =>
+ try {
+ pluginInfo.pluginClass.shutdown(instance)
+ } catch {
+ case e: Exception => {
+ logger.error(s"Error during plugin shutdown", e)
+ }
+ }
+ }
+ }
+
+
+}
+
+case class PluginInfo(
+ pluginId: String,
+ pluginName: String,
+ version: String,
+ description: String,
+ pluginClass: Plugin
+)
\ No newline at end of file
diff --git a/src/main/scala/plugin/Results.scala b/src/main/scala/plugin/Results.scala
new file mode 100644
index 0000000..18fdb7f
--- /dev/null
+++ b/src/main/scala/plugin/Results.scala
@@ -0,0 +1,11 @@
+package plugin
+
+import play.twirl.api.Html
+
+/**
+ * Defines result case classes returned by plugin controller.
+ */
+object Results {
+ case class Redirect(path: String)
+ case class Fragment(html: Html)
+}
diff --git a/src/main/scala/plugin/Sessions.scala b/src/main/scala/plugin/Sessions.scala
new file mode 100644
index 0000000..7398c9a
--- /dev/null
+++ b/src/main/scala/plugin/Sessions.scala
@@ -0,0 +1,11 @@
+package plugin
+
+import slick.jdbc.JdbcBackend.Session
+
+/**
+ * Provides Slick Session to Plug-ins.
+ */
+object Sessions {
+ val sessions = new ThreadLocal[Session]
+ implicit def session: Session = sessions.get()
+}
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
deleted file mode 100644
index 30610bc..0000000
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ /dev/null
@@ -1,245 +0,0 @@
-package servlet
-
-import java.io.File
-import java.sql.{DriverManager, Connection}
-import org.apache.commons.io.FileUtils
-import javax.servlet.{ServletContextListener, ServletContextEvent}
-import org.apache.commons.io.IOUtils
-import org.slf4j.LoggerFactory
-import util.Directory._
-import util.ControlUtil._
-import util.JDBCUtil._
-import org.eclipse.jgit.api.Git
-import util.{DatabaseConfig, Directory}
-
-object AutoUpdate {
-
- /**
- * Version of GitBucket
- *
- * @param majorVersion the major version
- * @param minorVersion the minor version
- */
- case class Version(majorVersion: Int, minorVersion: Int){
-
- 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.
- */
- def update(conn: Connection): Unit = {
- val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
-
- using(Thread.currentThread.getContextClassLoader.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}"
- }
-
- /**
- * The history of versions. A head of this sequence is the current BitBucket version.
- */
- val versions = Seq(
- new Version(2, 8),
- new Version(2, 7) {
- override def update(conn: Connection): Unit = {
- super.update(conn)
- conn.select("SELECT * FROM REPOSITORY"){ rs =>
- // Rename attached files directory from /issues to /comments
- val userName = rs.getString("USER_NAME")
- val repoName = rs.getString("REPOSITORY_NAME")
- defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
- val oldDir = new File(newDir.getParentFile, "issues")
- if(oldDir.exists && oldDir.isDirectory){
- oldDir.renameTo(newDir)
- }
- }
- // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
- val originalUserName = rs.getString("ORIGIN_USER_NAME")
- val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
- if(originalUserName != null && originalRepoName != null){
- if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
- originalUserName, originalRepoName) == 0){
- conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
- "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
- }
- }
- // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
- val parentUserName = rs.getString("PARENT_USER_NAME")
- val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
- if(parentUserName != null && parentRepoName != null){
- if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
- parentUserName, parentRepoName) == 0){
- conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
- "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
- }
- }
- }
- }
- },
- new Version(2, 6),
- new Version(2, 5),
- new Version(2, 4),
- new Version(2, 3) {
- override def update(conn: Connection): Unit = {
- super.update(conn)
- conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
- val curInfo = rs.getString("ADDITIONAL_INFO")
- val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
- if (curInfo != newInfo) {
- conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
- }
- }
- FileUtils.deleteDirectory(Directory.getPluginCacheDir())
- FileUtils.deleteDirectory(new File(Directory.PluginHome))
- }
- },
- new Version(2, 2),
- new Version(2, 1),
- new Version(2, 0){
- override def update(conn: Connection): Unit = {
- import eu.medsea.mimeutil.{MimeUtil2, MimeType}
-
- val mimeUtil = new MimeUtil2()
- mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
-
- super.update(conn)
- 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): Unit = {
- super.update(conn)
- // 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 BitBucket.
- */
- 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)
- }
-
-}
-
-/**
- * Update database schema automatically in the context initializing.
- */
-class AutoUpdateListener extends ServletContextListener {
- import AutoUpdate._
-
- private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
-// private val scheduler = StdSchedulerFactory.getDefaultScheduler
-
- override def contextInitialized(event: ServletContextEvent): Unit = {
- val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
- if(dataDir != null){
- System.setProperty("gitbucket.home", dataDir)
- }
- org.h2.Driver.load()
-
- defining(getConnection()){ conn =>
- logger.debug("Start schema update")
- try {
- defining(getCurrentVersion()){ currentVersion =>
- if(currentVersion == headVersion){
- logger.debug("No update")
- } else if(!versions.contains(currentVersion)){
- logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
- } else {
- versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
- FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
- logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
- }
- }
- } catch {
- case ex: Throwable => {
- logger.error("Failed to schema update", ex)
- ex.printStackTrace()
- conn.rollback()
- }
- }
- logger.debug("End schema update")
- }
- }
-
- def contextDestroyed(sce: ServletContextEvent): Unit = {
- }
-
- private def getConnection(): Connection =
- DriverManager.getConnection(
- DatabaseConfig.url,
- DatabaseConfig.user,
- DatabaseConfig.password)
-
-}
diff --git a/src/main/scala/servlet/InitializeListener.scala b/src/main/scala/servlet/InitializeListener.scala
new file mode 100644
index 0000000..fdc3c48
--- /dev/null
+++ b/src/main/scala/servlet/InitializeListener.scala
@@ -0,0 +1,204 @@
+package servlet
+
+import java.io.File
+import java.sql.{DriverManager, Connection}
+import org.apache.commons.io.FileUtils
+import javax.servlet.{ServletContextListener, ServletContextEvent}
+import org.slf4j.LoggerFactory
+import util.Directory._
+import util.ControlUtil._
+import util.JDBCUtil._
+import org.eclipse.jgit.api.Git
+import util.{Version, Versions}
+import plugin._
+import util.{DatabaseConfig, Directory}
+
+object AutoUpdate {
+
+ /**
+ * The history of versions. A head of this sequence is the current BitBucket version.
+ */
+ val versions = Seq(
+ 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 BitBucket.
+ */
+ 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)
+ }
+
+}
+
+/**
+ * Initialize GitBucket system.
+ * Update database schema and load plug-ins automatically in the context initializing.
+ */
+class InitializeListener extends ServletContextListener {
+ import AutoUpdate._
+
+ private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
+
+ override def contextInitialized(event: ServletContextEvent): Unit = {
+ val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
+ if(dataDir != null){
+ System.setProperty("gitbucket.home", dataDir)
+ }
+ org.h2.Driver.load()
+
+ defining(getConnection()){ conn =>
+ // Migration
+ logger.debug("Start schema update")
+ Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
+ FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
+ }
+ // Load plugins
+ logger.debug("Initialize plugins")
+ PluginRegistry.initialize(event.getServletContext, conn)
+ }
+
+ }
+
+ def contextDestroyed(event: ServletContextEvent): Unit = {
+ // Shutdown plugins
+ PluginRegistry.shutdown(event.getServletContext)
+ }
+
+ private def getConnection(): Connection =
+ DriverManager.getConnection(
+ DatabaseConfig.url,
+ DatabaseConfig.user,
+ DatabaseConfig.password)
+
+}
diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala
index c231fb0..7945f32 100644
--- a/src/main/scala/util/ControlUtil.scala
+++ b/src/main/scala/util/ControlUtil.scala
@@ -37,4 +37,10 @@
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
+ def ignore[T](f: => Unit): Unit = try {
+ f
+ } catch {
+ case e: Exception => ()
+ }
+
}
diff --git a/src/main/scala/util/JDBCUtil.scala b/src/main/scala/util/JDBCUtil.scala
index 375ea14..5d880d0 100644
--- a/src/main/scala/util/JDBCUtil.scala
+++ b/src/main/scala/util/JDBCUtil.scala
@@ -18,6 +18,14 @@
}
}
+ def find[T](sql: String, params: Any*)(f: ResultSet => T): Option[T] = {
+ execute(sql, params: _*){ stmt =>
+ using(stmt.executeQuery()){ rs =>
+ if(rs.next) Some(f(rs)) else None
+ }
+ }
+ }
+
def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>
diff --git a/src/main/scala/util/Version.scala b/src/main/scala/util/Version.scala
new file mode 100644
index 0000000..fed74a8
--- /dev/null
+++ b/src/main/scala/util/Version.scala
@@ -0,0 +1,67 @@
+package util
+
+import java.sql.Connection
+
+import org.apache.commons.io.IOUtils
+import org.slf4j.LoggerFactory
+import util.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/main.scala.html b/src/main/twirl/main.scala.html
index 9b8ae02..6d35ff1 100644
--- a/src/main/twirl/main.scala.html
+++ b/src/main/twirl/main.scala.html
@@ -82,5 +82,10 @@
});
});
+ @plugin.PluginRegistry().getJavaScript(request.getRequestURI).map { script =>
+
+ }