", 1, null)
+ result
+ } finally {
+ JsContext.exit
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/scala/plugin/Plugin.scala b/src/main/scala/plugin/Plugin.scala
new file mode 100644
index 000000000..59961fe4a
--- /dev/null
+++ b/src/main/scala/plugin/Plugin.scala
@@ -0,0 +1,16 @@
+package plugin
+
+import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
+
+trait Plugin {
+ val id: String
+ val version: String
+ val author: String
+ val url: String
+ val description: String
+
+ def repositoryMenus : List[RepositoryMenu]
+ def globalMenus : List[GlobalMenu]
+ def repositoryActions : List[Action]
+ def globalActions : List[Action]
+}
diff --git a/src/main/scala/plugin/PluginSystem.scala b/src/main/scala/plugin/PluginSystem.scala
new file mode 100644
index 000000000..e40f860bc
--- /dev/null
+++ b/src/main/scala/plugin/PluginSystem.scala
@@ -0,0 +1,123 @@
+package plugin
+
+import app.Context
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
+import org.slf4j.LoggerFactory
+import java.util.concurrent.atomic.AtomicBoolean
+import util.Directory._
+import util.ControlUtil._
+import org.apache.commons.io.FileUtils
+import util.JGitUtil
+import org.eclipse.jgit.api.Git
+
+/**
+ * Provides extension points to plug-ins.
+ */
+object PluginSystem {
+
+ private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
+
+ private val initialized = new AtomicBoolean(false)
+ private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
+ private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
+
+ def install(plugin: Plugin): Unit = {
+ pluginsMap.put(plugin.id, plugin)
+ }
+
+ def plugins: List[Plugin] = pluginsMap.values.toList
+
+ def uninstall(id: String): Unit = {
+ pluginsMap.remove(id)
+ }
+
+ def repositories: List[PluginRepository] = repositoriesList.toList
+
+ /**
+ * Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
+ */
+ def init(): Unit = {
+ if(initialized.compareAndSet(false, true)){
+ // Load installed plugins
+ val pluginDir = new java.io.File(PluginHome)
+ if(pluginDir.exists && pluginDir.isDirectory){
+ pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
+ installPlugin(dir.getName)
+ }
+ }
+ // Add default plugin repositories
+ repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
+ }
+ }
+
+ // TODO Method name seems to not so good.
+ def installPlugin(id: String): Unit = {
+ val pluginDir = new java.io.File(PluginHome)
+ val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js")
+
+ if(javaScriptFile.exists && javaScriptFile.isFile){
+ val properties = new java.util.Properties()
+ using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in =>
+ properties.load(in)
+ }
+
+ val script = FileUtils.readFileToString(javaScriptFile, "UTF-8")
+ try {
+ JavaScriptPlugin.evaluateJavaScript(script, Map(
+ "id" -> properties.getProperty("id"),
+ "version" -> properties.getProperty("version"),
+ "author" -> properties.getProperty("author"),
+ "url" -> properties.getProperty("url"),
+ "description" -> properties.getProperty("description")
+ ))
+ } catch {
+ case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e)
+ }
+ }
+ }
+
+ def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
+ def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
+ def repositoryActions : List[Action] = pluginsMap.values.flatMap(_.repositoryActions).toList
+ def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
+
+ // Case classes to hold plug-ins information internally in GitBucket
+ case class PluginRepository(id: String, url: String)
+ case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
+ case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
+ case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any)
+
+ /**
+ * Checks whether the plugin is updatable.
+ */
+ def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
+ if(oldVersion == newVersion){
+ false
+ } else {
+ val dim1 = oldVersion.split("\\.").map(_.toInt)
+ val dim2 = newVersion.split("\\.").map(_.toInt)
+ dim1.zip(dim2).foreach { case (a, b) =>
+ if(a < b){
+ return true
+ } else if(a > b){
+ return false
+ }
+ }
+ return false
+ }
+ }
+
+ // TODO This is a test
+// addGlobalMenu("Google", "http://www.google.co.jp/", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
+// { context => context.loginAccount.isDefined }
+//
+// addRepositoryMenu("Board", "board", "/board", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
+// { context => true}
+//
+// addGlobalAction("/hello"){ (request, response) =>
+// "Hello World!"
+// }
+
+}
+
+
diff --git a/src/main/scala/plugin/PluginUpdateJob.scala b/src/main/scala/plugin/PluginUpdateJob.scala
new file mode 100644
index 000000000..36a70e852
--- /dev/null
+++ b/src/main/scala/plugin/PluginUpdateJob.scala
@@ -0,0 +1,67 @@
+package plugin
+
+import util.Directory._
+import org.eclipse.jgit.api.Git
+import org.slf4j.LoggerFactory
+import org.quartz.{Scheduler, JobExecutionContext, Job}
+import org.quartz.JobBuilder._
+import org.quartz.TriggerBuilder._
+import org.quartz.SimpleScheduleBuilder._
+
+class PluginUpdateJob extends Job {
+
+ private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
+ private var failedCount = 0
+
+ /**
+ * Clone or pull all plugin repositories
+ *
+ * TODO Support plugin repository access through the proxy server
+ */
+ override def execute(context: JobExecutionContext): Unit = {
+ try {
+ if(failedCount > 3){
+ logger.error("Skip plugin information updating because failed count is over limit")
+ } else {
+ logger.info("Start plugin information updating")
+ PluginSystem.repositories.foreach { repository =>
+ logger.info(s"Updating ${repository.id}: ${repository.url}...")
+ val dir = getPluginCacheDir()
+ val repo = new java.io.File(dir, repository.id)
+ if(repo.exists){
+ // pull if the repository is already cloned
+ Git.open(repo).pull().call()
+ } else {
+ // clone if the repository is not exist
+ Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
+ }
+ }
+ logger.info("End plugin information updating")
+ }
+ } catch {
+ case e: Exception => {
+ failedCount = failedCount + 1
+ logger.error("Failed to update plugin information", e)
+ }
+ }
+ }
+}
+
+object PluginUpdateJob {
+
+ def schedule(scheduler: Scheduler): Unit = {
+// TODO Enable commented code to enable plug-in system
+// val job = newJob(classOf[PluginUpdateJob])
+// .withIdentity("pluginUpdateJob")
+// .build()
+//
+// val trigger = newTrigger()
+// .withIdentity("pluginUpdateTrigger")
+// .startNow()
+// .withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
+// .build()
+//
+// scheduler.scheduleJob(job, trigger)
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/scala/plugin/ScalaPlugin.scala b/src/main/scala/plugin/ScalaPlugin.scala
new file mode 100644
index 000000000..c0bb728f0
--- /dev/null
+++ b/src/main/scala/plugin/ScalaPlugin.scala
@@ -0,0 +1,38 @@
+package plugin
+
+import app.Context
+import scala.collection.mutable.ListBuffer
+import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
+
+// TODO This is a sample implementation for Scala based plug-ins.
+class ScalaPlugin(val id: String, val version: String,
+ val author: String, val url: String, val description: String) extends Plugin {
+
+ private val repositoryMenuList = ListBuffer[RepositoryMenu]()
+ private val globalMenuList = ListBuffer[GlobalMenu]()
+ private val repositoryActionList = ListBuffer[Action]()
+ private val globalActionList = ListBuffer[Action]()
+
+ def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
+ def globalMenus : List[GlobalMenu] = globalMenuList.toList
+ def repositoryActions : List[Action] = repositoryActionList.toList
+ def globalActions : List[Action] = globalActionList.toList
+
+ def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
+ repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
+ }
+
+ def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
+ globalMenuList += GlobalMenu(label, url, icon, condition)
+ }
+
+ def addGlobalAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
+ globalActionList += Action(path, function)
+ }
+
+ def addRepositoryAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
+ repositoryActionList += Action(path, function)
+ }
+
+}
diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala
index 46ac85ed6..70c907e9e 100644
--- a/src/main/scala/service/IssuesService.scala
+++ b/src/main/scala/service/IssuesService.scala
@@ -51,6 +51,7 @@ trait IssuesService {
repos: (String, String)*)(implicit s: Session): Int =
// TODO check SQL
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
+
/**
* Returns the Map which contains issue count for each labels.
*
diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala
index beb79b989..e40ca3c41 100644
--- a/src/main/scala/service/PullRequestService.scala
+++ b/src/main/scala/service/PullRequestService.scala
@@ -20,13 +20,13 @@ trait PullRequestService { self: IssuesService =>
.map(pr => pr.commitIdTo -> pr.commitIdFrom)
.update((commitIdTo, commitIdFrom))
- def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String])
+ def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])
(implicit s: Session): List[PullRequestCount] =
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t2.closed is closed.bind) &&
- (t1.userName is owner.bind) &&
+ (t1.userName is owner.get.bind, owner.isDefined) &&
(t1.repositoryName is repository.get.bind, repository.isDefined)
}
.groupBy { case (t1, t2) => t2.openedUserName }
diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala
index cdc72fe3e..c7e0afb21 100644
--- a/src/main/scala/service/RepositoryService.scala
+++ b/src/main/scala/service/RepositoryService.scala
@@ -156,13 +156,18 @@ trait RepositoryService { self: AccountService =>
}
}
- def getUserRepositories(userName: String, baseUrl: String)(implicit s: Session): List[RepositoryInfo] = {
+ def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
+ (implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName is userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
- JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
+ if(withoutPhysicalInfo){
+ new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
+ } else {
+ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
+ },
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
@@ -179,9 +184,12 @@ trait RepositoryService { self: AccountService =>
* @param loginAccount the logged in account
* @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
+ * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
+ * branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate.
*/
- def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None)
+ def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
+ withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match {
// for Administrators
@@ -197,7 +205,11 @@ trait RepositoryService { self: AccountService =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse LiteralColumn(true)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
- JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
+ if(withoutPhysicalInfo){
+ new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
+ } else {
+ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
+ },
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index 801ff68cb..e79faa5ac 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -1,193 +1,209 @@
-package servlet
-
-import java.io.File
-import java.sql.{DriverManager, Connection}
-import org.apache.commons.io.FileUtils
-import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
-import org.apache.commons.io.IOUtils
-import org.slf4j.LoggerFactory
-import util.Directory._
-import util.ControlUtil._
-import org.eclipse.jgit.api.Git
-import util.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, 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)
- using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
- while(rs.next){
- defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
- if(dir.exists && dir.isDirectory){
- dir.listFiles.foreach { file =>
- if(file.getName.indexOf('.') < 0){
- val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
- if(mimeType.startsWith("image/")){
- file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
- }
- }
- }
- }
- }
- }
- }
- }
- },
- 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
- using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
- while(rs.next){
- using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
- defining(git.getRepository.getConfig){ config =>
- if(!config.getBoolean("http", "receivepack", false)){
- config.setBoolean("http", null, "receivepack", true)
- config.save
- }
- }
- }
- }
- }
- }
- },
- 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])
-
- 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()
- event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
-
- logger.debug("Start schema update")
- defining(getConnection(event.getServletContext)){ conn =>
- 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")
- conn.commit()
- 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 = {
- // Nothing to do.
- }
-
- private def getConnection(servletContext: ServletContext): Connection =
- DriverManager.getConnection(
- servletContext.getInitParameter("db.url"),
- servletContext.getInitParameter("db.user"),
- servletContext.getInitParameter("db.password"))
-
-}
+package servlet
+
+import java.io.File
+import java.sql.{DriverManager, Connection}
+import org.apache.commons.io.FileUtils
+import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
+import org.apache.commons.io.IOUtils
+import org.slf4j.LoggerFactory
+import util.Directory._
+import util.ControlUtil._
+import org.eclipse.jgit.api.Git
+import util.Directory
+import plugin.PluginUpdateJob
+
+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, 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)
+ using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
+ while(rs.next){
+ defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
+ if(dir.exists && dir.isDirectory){
+ dir.listFiles.foreach { file =>
+ if(file.getName.indexOf('.') < 0){
+ val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
+ if(mimeType.startsWith("image/")){
+ file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ 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
+ using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
+ while(rs.next){
+ using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
+ defining(git.getRepository.getConfig){ config =>
+ if(!config.getBoolean("http", "receivepack", false)){
+ config.setBoolean("http", null, "receivepack", true)
+ config.save
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ 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 org.quartz.impl.StdSchedulerFactory
+ import org.quartz.JobBuilder._
+ import org.quartz.TriggerBuilder._
+ import org.quartz.SimpleScheduleBuilder._
+ import AutoUpdate._
+
+ private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
+ private val scheduler = StdSchedulerFactory.getDefaultScheduler
+
+ override def contextInitialized(event: ServletContextEvent): Unit = {
+ val datadir = event.getServletContext.getInitParameter("gitbucket.home")
+ if(datadir != null){
+ System.setProperty("gitbucket.home", datadir)
+ }
+ org.h2.Driver.load()
+ event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
+
+ logger.debug("Start schema update")
+ defining(getConnection(event.getServletContext)){ conn =>
+ 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")
+ conn.commit()
+ 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")
+
+ logger.debug("Starting plugin system...")
+ plugin.PluginSystem.init()
+
+ scheduler.start()
+ PluginUpdateJob.schedule(scheduler)
+ logger.debug("PluginUpdateJob is started.")
+
+ logger.debug("Plugin system is initialized.")
+ }
+
+ def contextDestroyed(sce: ServletContextEvent): Unit = {
+ scheduler.shutdown()
+ }
+
+ private def getConnection(servletContext: ServletContext): Connection =
+ DriverManager.getConnection(
+ servletContext.getInitParameter("db.url"),
+ servletContext.getInitParameter("db.user"),
+ servletContext.getInitParameter("db.password"))
+
+}
diff --git a/src/main/scala/servlet/PluginActionInvokeFilter.scala b/src/main/scala/servlet/PluginActionInvokeFilter.scala
new file mode 100644
index 000000000..6dbbe78f9
--- /dev/null
+++ b/src/main/scala/servlet/PluginActionInvokeFilter.scala
@@ -0,0 +1,81 @@
+package servlet
+
+import javax.servlet._
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
+import org.apache.commons.io.IOUtils
+import twirl.api.Html
+import service.{AccountService, RepositoryService, SystemSettingsService}
+import model.Account
+import util.{JGitUtil, Keys}
+
+class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
+
+ def init(config: FilterConfig) = {}
+
+ def destroy(): Unit = {}
+
+ def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
+ (req, res) match {
+ case (request: HttpServletRequest, response: HttpServletResponse) => {
+ Database(req.getServletContext) withTransaction { implicit session =>
+ val path = req.asInstanceOf[HttpServletRequest].getRequestURI
+ if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
+ chain.doFilter(req, res)
+ }
+ }
+ }
+ }
+ }
+
+ private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse): Boolean = {
+ plugin.PluginSystem.globalActions.find(_.path == path).map { action =>
+ val result = action.function(request, response)
+ result match {
+ case x: String => {
+ response.setContentType("text/html; charset=UTF-8")
+ val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
+ implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
+ val html = _root_.html.main("GitBucket", None)(Html(x))
+ IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
+ }
+ case x => {
+ // TODO returns as JSON?
+ response.setContentType("application/json; charset=UTF-8")
+
+ }
+ }
+ true
+ } getOrElse false
+ }
+
+ private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
+ (implicit session: model.simple.Session): Boolean = {
+ val elements = path.split("/")
+ if(elements.length > 3){
+ val owner = elements(1)
+ val name = elements(2)
+ val remain = elements.drop(3).mkString("/", "/", "")
+ getRepository(owner, name, "").flatMap { repository => // TODO fill baseUrl
+ plugin.PluginSystem.repositoryActions.find(_.path == remain).map { action =>
+ val result = action.function(request, response)
+ result match {
+ case x: String => {
+ response.setContentType("text/html; charset=UTF-8")
+ val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
+ implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
+ val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(x))) // TODO specify active side menu
+ IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
+ }
+ case x => {
+ // TODO returns as JSON?
+ response.setContentType("application/json; charset=UTF-8")
+
+ }
+ }
+ true
+ }
+ } getOrElse false
+ } else false
+ }
+
+}
diff --git a/src/main/scala/servlet/TransactionFilter.scala b/src/main/scala/servlet/TransactionFilter.scala
index 6773a5901..b4712e920 100644
--- a/src/main/scala/servlet/TransactionFilter.scala
+++ b/src/main/scala/servlet/TransactionFilter.scala
@@ -33,6 +33,7 @@ class TransactionFilter extends Filter {
}
object Database {
+
def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database =
slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"),
diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala
index 1d15f1a75..920b22a7e 100644
--- a/src/main/scala/util/Directory.scala
+++ b/src/main/scala/util/Directory.scala
@@ -34,6 +34,10 @@ object Directory {
val DatabaseHome = s"${GitBucketHome}/data"
+ val PluginHome = s"${GitBucketHome}/plugins"
+
+ val TemporaryHome = s"${GitBucketHome}/tmp"
+
/**
* Substance directory of the repository.
*/
@@ -55,13 +59,18 @@ object Directory {
* Root of temporary directories for the upload file.
*/
def getTemporaryDir(sessionId: String): File =
- new File(s"${GitBucketHome}/tmp/_upload/${sessionId}")
+ new File(s"${TemporaryHome}/_upload/${sessionId}")
/**
* Root of temporary directories for the specified repository.
*/
def getTemporaryDir(owner: String, repository: String): File =
- new File(s"${GitBucketHome}/tmp/${owner}/${repository}")
+ new File(s"${TemporaryHome}/${owner}/${repository}")
+
+ /**
+ * Root of plugin cache directory. Plugin repositories are cloned into this directory.
+ */
+ def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
/**
* Temporary directory which is used to create an archive to download repository contents.
diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala
index 48c4ad7dd..c68bdbf26 100644
--- a/src/main/scala/util/JGitUtil.scala
+++ b/src/main/scala/util/JGitUtil.scala
@@ -35,7 +35,11 @@ object JGitUtil {
* @param branchList the list of branch names
* @param tags the list of tags
*/
- case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo])
+ case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
+ def this(owner: String, name: String, baseUrl: String) = {
+ this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
+ }
+ }
/**
* The file data for the file list of the repository viewer.
diff --git a/src/main/scala/util/LDAPUtil.scala b/src/main/scala/util/LDAPUtil.scala
index 131a133c2..d107b9672 100644
--- a/src/main/scala/util/LDAPUtil.scala
+++ b/src/main/scala/util/LDAPUtil.scala
@@ -47,11 +47,11 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed."
){ conn =>
- findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
+ findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(LDAPUserInfo(
userName = getUserNameFromMailAddress(userName),
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
- findFullName(conn, userDN, fullNameAttribute)
+ findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}.getOrElse(userName),
mailAddress = mailAddress))
case None => Left("Can't find mail address.")
@@ -130,15 +130,15 @@ object LDAPUtil {
}
}
- private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
- defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
+ private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] =
+ defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results =>
if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None
}
- private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] =
- defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results =>
+ private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] =
+ defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None
diff --git a/src/main/twirl/admin/menu.scala.html b/src/main/twirl/admin/menu.scala.html
index 09bc2de3e..25cda196d 100644
--- a/src/main/twirl/admin/menu.scala.html
+++ b/src/main/twirl/admin/menu.scala.html
@@ -11,6 +11,9 @@
System Settings
+
+ Plugins
+
H2 Console
diff --git a/src/main/twirl/admin/plugins/available.scala.html b/src/main/twirl/admin/plugins/available.scala.html
new file mode 100644
index 000000000..fcf37a0cf
--- /dev/null
+++ b/src/main/twirl/admin/plugins/available.scala.html
@@ -0,0 +1,37 @@
+@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main("Plugins"){
+ @admin.html.menu("plugins"){
+ @tab("available")
+
+ }
+}
+
diff --git a/src/main/twirl/admin/plugins/console.scala.html b/src/main/twirl/admin/plugins/console.scala.html
new file mode 100644
index 000000000..3c4158e5a
--- /dev/null
+++ b/src/main/twirl/admin/plugins/console.scala.html
@@ -0,0 +1,37 @@
+@()(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main("JavaScript Console"){
+ @admin.html.menu("plugins"){
+ @tab("console")
+
+ }
+}
+
+
\ No newline at end of file
diff --git a/src/main/twirl/admin/plugins/installed.scala.html b/src/main/twirl/admin/plugins/installed.scala.html
new file mode 100644
index 000000000..f85c149e1
--- /dev/null
+++ b/src/main/twirl/admin/plugins/installed.scala.html
@@ -0,0 +1,47 @@
+@(plugins: List[plugin.Plugin],
+ updatablePlugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main("Plugins"){
+ @admin.html.menu("plugins"){
+ @tab("installed")
+
+ }
+}
+
diff --git a/src/main/twirl/admin/plugins/tab.scala.html b/src/main/twirl/admin/plugins/tab.scala.html
new file mode 100644
index 000000000..2e9d1ace7
--- /dev/null
+++ b/src/main/twirl/admin/plugins/tab.scala.html
@@ -0,0 +1,9 @@
+@(active: String)(implicit context: app.Context)
+@import context._
+
diff --git a/src/main/twirl/admin/users/list.scala.html b/src/main/twirl/admin/users/list.scala.html
index 942f6ee04..3b2029c2b 100644
--- a/src/main/twirl/admin/users/list.scala.html
+++ b/src/main/twirl/admin/users/list.scala.html
@@ -1,71 +1,71 @@
-@(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context)
-@import context._
-@import view.helpers._
-@html.main("Manage Users"){
- @admin.html.menu("users"){
-
-
-
- @users.map { account =>
-
-
-
- @if(account.isGroupAccount){
- Edit
- } else {
- Edit
- }
-
-
- @avatar(account.userName, 20)
- @account.userName
- @if(account.isGroupAccount){
- (Group)
- } else {
- @if(account.isAdmin){
- (Administrator)
- } else {
- (Normal)
- }
- }
- @if(account.isGroupAccount){
- @members(account.userName).map { userName =>
- @avatar(userName, 20, tooltip = true)
- }
- }
-
-
-
- @if(!account.isGroupAccount){
- @account.mailAddress
- }
- @account.url.map { url =>
- @url
- }
-
-
- Registered: @datetime(account.registeredDate)
- Updated: @datetime(account.updatedDate)
- @if(!account.isGroupAccount){
- Last Login: @account.lastLoginDate.map(datetime)
- }
-
- |
-
- }
-
- }
-}
-
\ No newline at end of file
diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html
index 3a2430c0b..fb022c0c2 100644
--- a/src/main/twirl/admin/users/user.scala.html
+++ b/src/main/twirl/admin/users/user.scala.html
@@ -1,80 +1,80 @@
-@(account: Option[model.Account])(implicit context: app.Context)
-@import context._
-@html.main(if(account.isEmpty) "New User" else "Update User"){
- @admin.html.menu("users"){
-
- }
-}
+@(account: Option[model.Account])(implicit context: app.Context)
+@import context._
+@html.main(if(account.isEmpty) "New User" else "Update User"){
+ @admin.html.menu("users"){
+
+ }
+}
diff --git a/src/main/twirl/helper/activities.scala.html b/src/main/twirl/helper/activities.scala.html
index 6e7f324c2..980eaf53c 100644
--- a/src/main/twirl/helper/activities.scala.html
+++ b/src/main/twirl/helper/activities.scala.html
@@ -1,98 +1,98 @@
-@(activities: List[model.Activity])(implicit context: app.Context)
-@import context._
-@import view.helpers._
-
-@if(activities.isEmpty){
- No activity
-} else {
- @activities.map { activity =>
-
- @(activity.activityType match {
- case "open_issue" => detailActivity(activity, "activity-issue.png")
- case "comment_issue" => detailActivity(activity, "activity-comment.png")
- case "close_issue" => detailActivity(activity, "activity-issue-close.png")
- case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
- case "open_pullreq" => detailActivity(activity, "activity-merge.png")
- case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
- case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
- case "create_branch" => simpleActivity(activity, "activity-branch.png")
- case "delete_branch" => simpleActivity(activity, "activity-delete.png")
- case "create_tag" => simpleActivity(activity, "activity-tag.png")
- case "delete_tag" => simpleActivity(activity, "activity-delete.png")
- case "fork" => simpleActivity(activity, "activity-fork.png")
- case "push" => customActivity(activity, "activity-commit.png"){
-
- {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
- if(i == 3){
-
...
- } else {
- if(commit.nonEmpty){
-
- }
- }
- }}
-
- }
- case "create_wiki" => customActivity(activity, "activity-wiki.png"){
-
- }
- case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
- activity.additionalInfo.get.split(":") match {
- case Array(pageName, commitId) =>
-
- case Array(pageName) =>
-
- }
- }
- })
-
- }
-}
-
-@detailActivity(activity: model.Activity, image: String) = {
- 
-
-
@datetime(activity.activityDate)
-
- @avatar(activity.activityUserName, 16)
- @activityMessage(activity.message)
-
- @activity.additionalInfo.map { additionalInfo =>
-
@additionalInfo
- }
-
-}
-
-@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
-
-
-
@datetime(activity.activityDate)
-
- @avatar(activity.activityUserName, 16)
- @activityMessage(activity.message)
-
- @additionalInfo
-
-}
-
-@simpleActivity(activity: model.Activity, image: String) = {
-
-
-
- @avatar(activity.activityUserName, 16)
- @activityMessage(activity.message)
- @datetime(activity.activityDate)
-
-
-}
-
+@(activities: List[model.Activity])(implicit context: app.Context)
+@import context._
+@import view.helpers._
+
+@if(activities.isEmpty){
+ No activity
+} else {
+ @activities.map { activity =>
+
+ @(activity.activityType match {
+ case "open_issue" => detailActivity(activity, "activity-issue.png")
+ case "comment_issue" => detailActivity(activity, "activity-comment.png")
+ case "close_issue" => detailActivity(activity, "activity-issue-close.png")
+ case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
+ case "open_pullreq" => detailActivity(activity, "activity-merge.png")
+ case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
+ case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
+ case "create_branch" => simpleActivity(activity, "activity-branch.png")
+ case "delete_branch" => simpleActivity(activity, "activity-delete.png")
+ case "create_tag" => simpleActivity(activity, "activity-tag.png")
+ case "delete_tag" => simpleActivity(activity, "activity-delete.png")
+ case "fork" => simpleActivity(activity, "activity-fork.png")
+ case "push" => customActivity(activity, "activity-commit.png"){
+
+ {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
+ if(i == 3){
+
...
+ } else {
+ if(commit.nonEmpty){
+
+ }
+ }
+ }}
+
+ }
+ case "create_wiki" => customActivity(activity, "activity-wiki.png"){
+
+ }
+ case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
+ activity.additionalInfo.get.split(":") match {
+ case Array(pageName, commitId) =>
+
+ case Array(pageName) =>
+
+ }
+ }
+ })
+
+ }
+}
+
+@detailActivity(activity: model.Activity, image: String) = {
+ 
+
+
@datetime(activity.activityDate)
+
+ @avatar(activity.activityUserName, 16)
+ @activityMessage(activity.message)
+
+ @activity.additionalInfo.map { additionalInfo =>
+
@additionalInfo
+ }
+
+}
+
+@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
+
+
+
@datetime(activity.activityDate)
+
+ @avatar(activity.activityUserName, 16)
+ @activityMessage(activity.message)
+
+ @additionalInfo
+
+}
+
+@simpleActivity(activity: model.Activity, image: String) = {
+
+
+
+ @avatar(activity.activityUserName, 16)
+ @activityMessage(activity.message)
+ @datetime(activity.activityDate)
+
+
+}
+
diff --git a/src/main/twirl/helper/diff.scala.html b/src/main/twirl/helper/diff.scala.html
index 551e3fae4..e1ab2d768 100644
--- a/src/main/twirl/helper/diff.scala.html
+++ b/src/main/twirl/helper/diff.scala.html
@@ -1,105 +1,105 @@
-@(diffs: Seq[util.JGitUtil.DiffInfo],
- repository: service.RepositoryService.RepositoryInfo,
- newCommitId: Option[String],
- oldCommitId: Option[String],
- showIndex: Boolean)(implicit context: app.Context)
-@import context._
-@import view.helpers._
-@import org.eclipse.jgit.diff.DiffEntry.ChangeType
-@if(showIndex){
-
-
-
-
- Showing @diffs.size changed @plural(diffs.size, "file")
-
-
-}
-@diffs.zipWithIndex.map { case (diff, i) =>
-
-
-
-
-
-
- |
- @if(diff.newContent != None || diff.oldContent != None){
-
-
-
- } else {
- Not supported
- }
- |
-
-
-}
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/main/twirl/helper/preview.scala.html b/src/main/twirl/helper/preview.scala.html
index 185cccf81..b6f37c768 100644
--- a/src/main/twirl/helper/preview.scala.html
+++ b/src/main/twirl/helper/preview.scala.html
@@ -25,8 +25,8 @@
-
-
+
+
+@(collaborators: List[String],
+ milestones: List[model.Milestone],
+ labels: List[model.Label],
+ hasWritePermission: Boolean,
+ repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
+ @html.menu("issues", repository){
+ @tab("", true, repository)
+
+ }
+}
+
diff --git a/src/main/twirl/issues/issuedetail.scala.html b/src/main/twirl/issues/issuedetail.scala.html
index 4eae1ee5c..ac9101ef5 100644
--- a/src/main/twirl/issues/issuedetail.scala.html
+++ b/src/main/twirl/issues/issuedetail.scala.html
@@ -116,7 +116,7 @@ $(function(){
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('').attr('href', '@path/' + userName).text(userName))
.append(' is assigned');
- $('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
+ $('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
});
});
diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html
index b3ab4e0c0..d40112113 100644
--- a/src/main/twirl/main.scala.html
+++ b/src/main/twirl/main.scala.html
@@ -9,26 +9,26 @@
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+