Merge branch 'master' into slick2

Conflicts:
	project/build.scala
	src/main/scala/app/IndexController.scala
	src/main/scala/app/RepositorySettingsController.scala
	src/main/scala/model/Account.scala
	src/main/scala/model/BasicTemplate.scala
	src/main/scala/model/Issue.scala
	src/main/scala/model/IssueComment.scala
	src/main/scala/model/package.scala
	src/main/scala/service/IssuesService.scala
	src/main/scala/service/PullRequestService.scala
	src/main/scala/service/RepositoryService.scala
	src/main/scala/service/WikiService.scala
	src/main/scala/servlet/TransactionFilter.scala
	src/main/scala/util/Notifier.scala
This commit is contained in:
Naoki Takezoe
2014-07-06 17:02:49 +09:00
287 changed files with 98327 additions and 61369 deletions

View File

@@ -80,6 +80,11 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 2.1 - xx Sep 2014
- Upgrade to Slick 2.0 from 1.9
- Base part of the plug-in system is merged
- Many bug fix and improvements
### 2.0 - 31 May 2014
- Modern Github UI
- Preview in AceEditor

View File

@@ -50,8 +50,8 @@
</target>
<target name="rename" depends="embed">
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
<move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="all" depends="rename">

View File

@@ -39,7 +39,9 @@ object MyBuild extends Build {
"org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.0.2",
"org.mozilla" % "rhino" % "1.7R4",
"com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",

View File

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

View File

@@ -292,7 +292,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository.
*/
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
@@ -355,7 +355,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
// redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}")

View File

@@ -96,7 +96,7 @@ trait DashboardControllerBase extends ControllerBase {
condition,
None,
false),
getPullRequestCountGroupByUser(condition.state == "closed", userName, None),
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
getRepositoryNamesOfUser(userName).map { RepoName =>
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
}.sortBy(_._3).reverse,

View File

@@ -22,8 +22,8 @@ trait IndexControllerBase extends ControllerBase {
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil)
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
}

View File

@@ -13,7 +13,6 @@ import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
@@ -124,7 +123,7 @@ trait PullRequestsControllerBase extends ControllerBase {
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){
LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close.
@@ -367,7 +366,7 @@ trait PullRequestsControllerBase extends ControllerBase {
*/
private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
LockUtil.lock(s"${userName}/${repositoryName}"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
@@ -403,7 +402,7 @@ trait PullRequestsControllerBase extends ControllerBase {
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge") {
LockUtil.lock(s"${userName}/${repositoryName}") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
@@ -466,7 +465,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
userName,
page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ trait AccountComponent { self: Profile =>
}
case class Account(
userName: String,
fullName: String,
mailAddress: String,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import util.Directory._
import util.ControlUtil._
import org.eclipse.jgit.api.Git
import util.Directory
import plugin.PluginUpdateJob
object AutoUpdate {
@@ -143,8 +144,14 @@ object AutoUpdate {
* 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")
@@ -178,10 +185,19 @@ class AutoUpdateListener extends ServletContextListener {
}
}
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 = {
// Nothing to do.
scheduler.shutdown()
}
private def getConnection(servletContext: ServletContext): Connection =

View File

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

View File

@@ -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"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -116,7 +116,7 @@ $(function(){
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').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');
}
});
});

View File

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

View File

@@ -23,6 +23,13 @@
</li>
}
@sidemenuPlugin(path: String, name: String, label: String, icon: String) = {
<li @if(active == name){class="active"}>
<div class="@if(active == name){margin} else {gradient} pull-left"></div>
<a href="@url(repository)@path"><img src="@icon"/>@if(expand){ @label}</a>
</li>
}
<div class="container">
@if(repository.commitCount > 0){
<div class="pull-right">
@@ -54,6 +61,11 @@
@sidemenu("/issues", "issues", "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "Wiki")
@plugin.PluginSystem.repositoryMenus.map { menu =>
@if(menu.condition(context)){
@sidemenuPlugin(menu.url, menu.label, menu.label, menu.icon)
}
}
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@sidemenu("/settings", "settings", "Settings")
}

View File

@@ -77,7 +77,7 @@
</table>
}
}
<script src="@assets/common/js/jquery.ba-hashchange.js"></script>
<script src="@assets/vendors/jquery/jquery.ba-hashchange.js"></script>
<script>
$(window).load(function(){
$(window).hashchange(function(){

View File

@@ -51,9 +51,9 @@
</form>
}
}
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script>
$(function(){
diffUsingJS('oldText', 'newText', 'diffText');

View File

@@ -68,10 +68,10 @@
</form>
}
}
<script src="@assets/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script>
$(function(){
$('#editor').text($('#initial').val());

View File

@@ -43,7 +43,7 @@
<div class="small">
<strong>Clone this wiki locally</strong>
</div>
@helper.html.copy("repository-url-copy", repository.httpUrl){
@helper.html.copy("repository-url-copy", httpUrl(repository)){
<input type="text" value="@httpUrl(repository)" id="repository-url" style="width: 160px;" readonly>
}
@if(settings.ssh && loginAccount.isDefined){

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,6 @@
/****************************************************************************/
body {
color: #333;
line-height: 100%;
}
li p {
@@ -98,6 +97,13 @@ div.input-prepend span.count {
padding-bottom: 6px;
}
img.plugin-global-menu {
width: 16px;
height: 16px;
position: relative;
top: -2px;
}
/* ======================================================================== */
/* General Styles */
/* ======================================================================== */
@@ -859,12 +865,58 @@ ul.collaborator a.remove {
/****************************************************************************/
/* Markdown */
/****************************************************************************/
div.markdown-body {
line-height: 1.7;
font: 15px Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
word-wrap: break-word;
}
div.markdown-body h1 {
border-bottom: 1px solid #ddd;
font-size: 2.5em;
font-weight: bold;
}
div.markdown-body h2 {
border-bottom: 1px solid #eee;
font-size: 2em;
margin-top: 30px;
}
div.markdown-body h3 {
font-size: 1.5em;
}
div.markdown-body h4 {
font-size: 1.2em;
}
div.markdown-body h5 {
font-size: 1em;
}
div.markdown-body h6 {
color:#777;
font-size: 1em;
}
div.markdown-body li {
line-height: 1.7;
}
div.markdown-body p {
margin: 15px 0;
line-height: 1.7;
}
div.markdown-body pre {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
white-space: pre;
}
div.markdown-body code {
font-size: 12px;
padding: 0 5px;
}
div.markdown-body table {
@@ -1002,6 +1054,7 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
.markdown-head {
position: relative;
line-height: 1.7;
}
a.markdown-anchor-link {
@@ -1015,25 +1068,25 @@ a.markdown-anchor-link {
}
h1 a.markdown-anchor-link {
top: 8px;
top: 24px;
}
h2 a.markdown-anchor-link {
top: 6px;
top: 20px;
}
h3 a.markdown-anchor-link {
top: 6px;
top: 12px;
}
h4 a.markdown-anchor-link {
top: 2px;
top: 8px;
}
h5 a.markdown-anchor-link {
top: 2px;
top: 6px;
}
h6 a.markdown-anchor-link {
top: 2px;
top: 6px;
}

View File

@@ -103,3 +103,7 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
viewType: 1
}));
}
function jqSelectorEscape(val) {
return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
}

View File

@@ -59,6 +59,8 @@ var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var DiffHighlightRules = function() {
this.$rules = {
"start" : [{
regex: "^(?:\\*{15}|={67}|-{3}|\\+{3})$",

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