mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-06 13:35:50 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/*")
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}")
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ trait AccountComponent { self: Profile =>
|
||||
}
|
||||
|
||||
case class Account(
|
||||
|
||||
userName: String,
|
||||
fullName: String,
|
||||
mailAddress: String,
|
||||
|
||||
88
src/main/scala/plugin/JavaScriptPlugin.scala
Normal file
88
src/main/scala/plugin/JavaScriptPlugin.scala
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
16
src/main/scala/plugin/Plugin.scala
Normal file
16
src/main/scala/plugin/Plugin.scala
Normal 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]
|
||||
}
|
||||
123
src/main/scala/plugin/PluginSystem.scala
Normal file
123
src/main/scala/plugin/PluginSystem.scala
Normal 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!"
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
67
src/main/scala/plugin/PluginUpdateJob.scala
Normal file
67
src/main/scala/plugin/PluginUpdateJob.scala
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
38
src/main/scala/plugin/ScalaPlugin.scala
Normal file
38
src/main/scala/plugin/ScalaPlugin.scala
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 =
|
||||
|
||||
81
src/main/scala/servlet/PluginActionInvokeFilter.scala
Normal file
81
src/main/scala/servlet/PluginActionInvokeFilter.scala
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
37
src/main/twirl/admin/plugins/available.scala.html
Normal file
37
src/main/twirl/admin/plugins/available.scala.html
Normal 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>
|
||||
37
src/main/twirl/admin/plugins/console.scala.html
Normal file
37
src/main/twirl/admin/plugins/console.scala.html
Normal 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>
|
||||
47
src/main/twirl/admin/plugins/installed.scala.html
Normal file
47
src/main/twirl/admin/plugins/installed.scala.html
Normal 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>
|
||||
9
src/main/twirl/admin/plugins/tab.scala.html
Normal file
9
src/main/twirl/admin/plugins/tab.scala.html
Normal 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>
|
||||
@@ -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){
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -103,3 +103,7 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
|
||||
viewType: 1
|
||||
}));
|
||||
}
|
||||
|
||||
function jqSelectorEscape(val) {
|
||||
return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user