mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-05 04:56:02 +01:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b6a590bf | ||
|
|
285ef02a17 | ||
|
|
18375c741e | ||
|
|
21030344cc | ||
|
|
a494027217 | ||
|
|
7bca01af59 | ||
|
|
acf3fa9980 | ||
|
|
c0ce0f8d19 | ||
|
|
56e7168461 | ||
|
|
c2d0d94f05 | ||
|
|
fc22cfbbdd | ||
|
|
d62adbf649 | ||
|
|
dba5539e3e | ||
|
|
f0a8b3bb17 | ||
|
|
f52e7e1bdd | ||
|
|
58ba26f21e | ||
|
|
bf7b30630c | ||
|
|
b5cac0308e | ||
|
|
373ea39048 | ||
|
|
427f5eec5f | ||
|
|
a4e9903e00 | ||
|
|
0d900a892c | ||
|
|
dc6fdaf482 | ||
|
|
b79498ed9f | ||
|
|
69e8f628df | ||
|
|
d3d8e3ce5f | ||
|
|
0499c47f4b | ||
|
|
7fd0cdd7d8 | ||
|
|
49eaf79e01 | ||
|
|
3a96c30aa8 | ||
|
|
6d550fa485 | ||
|
|
7f9d69bb51 | ||
|
|
3cc7bd3cdb |
2
LICENSE
2
LICENSE
@@ -187,7 +187,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
Copyright 2013-2016 GitBucket Team
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ Support
|
|||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
-------------
|
-------------
|
||||||
|
### 4.4 - 28 Aug 2016
|
||||||
|
- Import a SQL dump file to the database
|
||||||
|
- `go get` support in private repositories
|
||||||
|
- Sort milestones by due date
|
||||||
|
- apache-sshd has been updated to 1.2.0
|
||||||
|
|
||||||
### 4.3 - 30 Jul 2016
|
### 4.3 - 30 Jul 2016
|
||||||
- Emoji support by [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
|
- Emoji support by [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
|
||||||
- User name suggestion
|
- User name suggestion
|
||||||
|
|||||||
59
build.sbt
59
build.sbt
@@ -1,6 +1,6 @@
|
|||||||
val Organization = "gitbucket"
|
val Organization = "io.github.gitbucket"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val GitBucketVersion = "4.3.0"
|
val GitBucketVersion = "4.4.0"
|
||||||
val ScalatraVersion = "2.4.1"
|
val ScalatraVersion = "2.4.1"
|
||||||
val JettyVersion = "9.3.9.v20160517"
|
val JettyVersion = "9.3.9.v20160517"
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ libraryDependencies ++= Seq(
|
|||||||
"org.apache.commons" % "commons-compress" % "1.11",
|
"org.apache.commons" % "commons-compress" % "1.11",
|
||||||
"org.apache.commons" % "commons-email" % "1.4",
|
"org.apache.commons" % "commons-email" % "1.4",
|
||||||
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
||||||
"org.apache.sshd" % "apache-sshd" % "1.0.0",
|
"org.apache.sshd" % "apache-sshd" % "1.2.0",
|
||||||
"org.apache.tika" % "tika-core" % "1.13",
|
"org.apache.tika" % "tika-core" % "1.13",
|
||||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
@@ -170,3 +170,56 @@ Keys.artifact in (Compile, executableKey) ~= {
|
|||||||
}
|
}
|
||||||
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
||||||
*/
|
*/
|
||||||
|
publishTo <<= version { (v: String) =>
|
||||||
|
val nexus = "https://oss.sonatype.org/"
|
||||||
|
if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
|
||||||
|
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
|
||||||
|
}
|
||||||
|
publishMavenStyle := true
|
||||||
|
pomIncludeRepository := { _ => false }
|
||||||
|
artifact in Keys.`package` := Artifact(moduleName.value)
|
||||||
|
pomExtra := (
|
||||||
|
<url>https://github.com/gitbucket/gitbucket</url>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>The Apache Software License, Version 2.0</name>
|
||||||
|
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
<scm>
|
||||||
|
<url>https://github.com/gitbucket/gitbucket</url>
|
||||||
|
<connection>scm:git:https://github.com/gitbucket/gitbucket.git</connection>
|
||||||
|
</scm>
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<id>takezoe</id>
|
||||||
|
<name>Naoki Takezoe</name>
|
||||||
|
<url>https://github.com/takezoe</url>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>shimamoto</id>
|
||||||
|
<name>Takako Shimamoto</name>
|
||||||
|
<url>https://github.com/shimamoto</url>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>tanacasino</id>
|
||||||
|
<name>Tomofumi Tanaka</name>
|
||||||
|
<url>https://github.com/tanacasino</url>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>mrkm4ntr</id>
|
||||||
|
<name>Shintaro Murakami</name>
|
||||||
|
<url>https://github.com/mrkm4ntr</url>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>nazoking</id>
|
||||||
|
<name>nazoking</name>
|
||||||
|
<url>https://github.com/nazoking</url>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>McFoggy</id>
|
||||||
|
<name>Matthieu Brouillard</name>
|
||||||
|
<url>https://github.com/McFoggy</url>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
|
|||||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
|
||||||
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
|
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
|
||||||
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
|
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
|
||||||
|
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3")
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ public class JettyLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server.setHandler(context);
|
server.setHandler(context);
|
||||||
|
server.setStopAtShutdown(true);
|
||||||
|
server.setStopTimeout(7_000);
|
||||||
server.start();
|
server.start();
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ object GitBucketCoreModule extends Module("gitbucket-core",
|
|||||||
new LiquibaseMigration("update/gitbucket-core_4.2.xml")
|
new LiquibaseMigration("update/gitbucket-core_4.2.xml")
|
||||||
),
|
),
|
||||||
new Version("4.2.1"),
|
new Version("4.2.1"),
|
||||||
new Version("4.3.0")
|
new Version("4.3.0"),
|
||||||
|
new Version("4.4.0")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
case class PersonalTokenForm(note: String)
|
case class PersonalTokenForm(note: String)
|
||||||
|
|
||||||
val newForm = mapping(
|
val newForm = mapping(
|
||||||
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
|
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||||
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
|
||||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
@@ -68,7 +68,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
|
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
|
||||||
|
|
||||||
val newGroupForm = mapping(
|
val newGroupForm = mapping(
|
||||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
"members" -> trim(label("Members" ,text(required, members)))
|
"members" -> trim(label("Members" ,text(required, members)))
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ trait ApiControllerBase extends ControllerBase {
|
|||||||
*/
|
*/
|
||||||
get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository =>
|
get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository =>
|
||||||
val (id, path) = repository.splitPath(multiParams("splat").head)
|
val (id, path) = repository.splitPath(multiParams("splat").head)
|
||||||
val refStr = params("ref")
|
val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
|
||||||
using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git =>
|
using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git =>
|
||||||
if (path.isEmpty) {
|
if (path.isEmpty) {
|
||||||
JsonFormat(getFileList(git, refStr, ".").map{f => ApiContents(f)})
|
JsonFormat(getFileList(git, refStr, ".").map{f => ApiContents(f)})
|
||||||
|
|||||||
@@ -244,4 +244,13 @@ trait AccountManagementControllerBase extends ControllerBase {
|
|||||||
.map { _ => "Mail address is already registered." }
|
.map { _ => "Mail address is already registered." }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val allReservedNames = Set("git", "admin", "upload", "api")
|
||||||
|
protected def reservedNames(): Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){
|
||||||
|
Some(s"${value} is reserved")
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,15 +79,10 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
|
|||||||
}
|
}
|
||||||
|
|
||||||
post("/import") {
|
post("/import") {
|
||||||
|
import JDBCUtil._
|
||||||
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
|
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
|
||||||
execute({ (file, fileId) =>
|
execute({ (file, fileId) =>
|
||||||
if(file.getName.endsWith(".xml")){
|
request2Session(request).conn.importAsSQL(file.getInputStream)
|
||||||
import JDBCUtil._
|
|
||||||
val conn = request2Session(request).conn
|
|
||||||
conn.importAsXML(file.getInputStream)
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Import is available for only the XML file.")
|
|
||||||
}
|
|
||||||
}, _ => true)
|
}, _ => true)
|
||||||
}
|
}
|
||||||
redirect("/admin/data")
|
redirect("/admin/data")
|
||||||
|
|||||||
@@ -117,9 +117,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
/**
|
/**
|
||||||
* Displays the file list of the repository root and the default branch.
|
* Displays the file list of the repository root and the default branch.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository")(referrersOnly {
|
get("/:owner/:repository") {
|
||||||
fileList(_)
|
params.get("go-get") match {
|
||||||
})
|
case Some("1") => defining(request.paths){ paths =>
|
||||||
|
getRepository(paths(0), paths(1)).map(gitbucket.core.html.goget(_))getOrElse NotFound()
|
||||||
|
}
|
||||||
|
case _ => referrersOnly(fileList(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the file list of the specified path and branch.
|
* Displays the file list of the specified path and branch.
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import gitbucket.core.util.ControlUtil._
|
|||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.util.StringUtil._
|
import gitbucket.core.util.StringUtil._
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
|
|
||||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||||
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
|
|
||||||
import org.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
@@ -105,7 +103,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
|
|
||||||
val newUserForm = mapping(
|
val newUserForm = mapping(
|
||||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||||
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
@@ -127,7 +125,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
)(EditUserForm.apply)
|
)(EditUserForm.apply)
|
||||||
|
|
||||||
val newGroupForm = mapping(
|
val newGroupForm = mapping(
|
||||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
"members" -> trim(label("Members" ,text(required, members)))
|
"members" -> trim(label("Members" ,text(required, members)))
|
||||||
@@ -176,11 +174,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/plugins")(adminOnly {
|
get("/admin/plugins")(adminOnly {
|
||||||
val manager = new JDBCVersionManager(request2Session(request).conn)
|
html.plugins(PluginRegistry().getPlugins())
|
||||||
val plugins = PluginRegistry().getPlugins().map { plugin =>
|
|
||||||
(plugin, manager.getCurrentVersion(plugin.pluginId))
|
|
||||||
}
|
|
||||||
html.plugins(plugins)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -309,12 +303,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
post("/admin/export")(adminOnly {
|
post("/admin/export")(adminOnly {
|
||||||
import gitbucket.core.util.JDBCUtil._
|
import gitbucket.core.util.JDBCUtil._
|
||||||
val session = request2Session(request)
|
val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
|
||||||
val file = if(params("type") == "sql"){
|
|
||||||
session.conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
|
|
||||||
} else {
|
|
||||||
session.conn.exportAsXML(request.getParameterValues("tableNames").toSeq)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName)
|
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import gitbucket.core.util.ControlUtil._
|
|||||||
import gitbucket.core.util.DatabaseConfig
|
import gitbucket.core.util.DatabaseConfig
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import io.github.gitbucket.solidbase.Solidbase
|
import io.github.gitbucket.solidbase.Solidbase
|
||||||
|
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
|
||||||
import io.github.gitbucket.solidbase.model.Module
|
import io.github.gitbucket.solidbase.model.Module
|
||||||
import org.apache.commons.codec.binary.{Base64, StringUtils}
|
import org.apache.commons.codec.binary.{Base64, StringUtils}
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -161,6 +162,8 @@ object PluginRegistry {
|
|||||||
*/
|
*/
|
||||||
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
|
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
|
||||||
val pluginDir = new File(PluginHome)
|
val pluginDir = new File(PluginHome)
|
||||||
|
val manager = new JDBCVersionManager(conn)
|
||||||
|
|
||||||
if(pluginDir.exists && pluginDir.isDirectory){
|
if(pluginDir.exists && pluginDir.isDirectory){
|
||||||
pluginDir.listFiles(new FilenameFilter {
|
pluginDir.listFiles(new FilenameFilter {
|
||||||
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
|
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
|
||||||
@@ -173,6 +176,13 @@ object PluginRegistry {
|
|||||||
val solidbase = new Solidbase()
|
val solidbase = new Solidbase()
|
||||||
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
|
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
|
||||||
|
|
||||||
|
// Check version
|
||||||
|
val databaseVersion = manager.getCurrentVersion(plugin.pluginId)
|
||||||
|
val pluginVersion = plugin.versions.last.getVersion
|
||||||
|
if(databaseVersion != pluginVersion){
|
||||||
|
throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}")
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
plugin.initialize(instance, context, settings)
|
plugin.initialize(instance, context, settings)
|
||||||
instance.addPlugin(PluginInfo(
|
instance.addPlugin(PluginInfo(
|
||||||
@@ -185,7 +195,7 @@ object PluginRegistry {
|
|||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
case e: Throwable => {
|
case e: Throwable => {
|
||||||
logger.error(s"Error during plugin initialization", e)
|
logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ trait AccountService {
|
|||||||
|
|
||||||
def getGroupNames(userName: String)(implicit s: Session): List[String] = {
|
def getGroupNames(userName: String)(implicit s: Session): List[String] = {
|
||||||
List(userName) ++
|
List(userName) ++
|
||||||
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list
|
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list.distinct
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ trait MilestonesService {
|
|||||||
|
|
||||||
def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
|
def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
|
||||||
val counts = Issues
|
val counts = Issues
|
||||||
.filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) }
|
.filter { t => t.byRepository(owner, repository) && (t.milestoneId.? isDefined) }
|
||||||
.groupBy { t => t.milestoneId -> t.closed }
|
.groupBy { t => t.milestoneId -> t.closed }
|
||||||
.map { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
|
.map { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
|
||||||
.toMap
|
.toMap
|
||||||
@@ -52,6 +52,6 @@ trait MilestonesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] =
|
def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] =
|
||||||
Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
|
Milestones.filter(_.byRepository(owner, repository)).sortBy(t => (t.dueDate.asc, t.closedDate.desc, t.milestoneId.desc)).list
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.apache.commons.io.FileUtils
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import akka.actor.{Actor, Props, ActorSystem}
|
import akka.actor.{Actor, Props, ActorSystem}
|
||||||
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
|
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize GitBucket system.
|
* Initialize GitBucket system.
|
||||||
@@ -80,8 +81,14 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
|||||||
// Rescue code for users who updated from 3.14 to 4.0.0
|
// Rescue code for users who updated from 3.14 to 4.0.0
|
||||||
// https://github.com/gitbucket/gitbucket/issues/1227
|
// https://github.com/gitbucket/gitbucket/issues/1227
|
||||||
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
|
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
|
||||||
if(currentVersion == "4.0"){
|
val databaseVersion = if(currentVersion == "4.0"){
|
||||||
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
|
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
|
||||||
|
"4.0.0"
|
||||||
|
} else currentVersion
|
||||||
|
|
||||||
|
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
|
||||||
|
if(databaseVersion != gitbucketVersion){
|
||||||
|
throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load plugins
|
// Load plugins
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import ControlUtil._
|
|||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import Directory._
|
import Directory._
|
||||||
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
|
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
|
||||||
import org.apache.sshd.server.command.UnknownCommand
|
import org.apache.sshd.server.scp.UnknownCommand
|
||||||
import org.eclipse.jgit.errors.RepositoryNotFoundException
|
import org.eclipse.jgit.errors.RepositoryNotFoundException
|
||||||
|
|
||||||
object GitCommand {
|
object GitCommand {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import gitbucket.core.service.SshKeyService
|
|||||||
import gitbucket.core.servlet.Database
|
import gitbucket.core.servlet.Database
|
||||||
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
|
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
|
||||||
import org.apache.sshd.server.session.ServerSession
|
import org.apache.sshd.server.session.ServerSession
|
||||||
import org.apache.sshd.common.session.Session
|
import org.apache.sshd.common.AttributeStore
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
object PublicKeyAuthenticator {
|
object PublicKeyAuthenticator {
|
||||||
// put in the ServerSession here to be read by GitCommand later
|
// put in the ServerSession here to be read by GitCommand later
|
||||||
private val userNameSessionKey = new Session.AttributeKey[String]
|
private val userNameSessionKey = new AttributeStore.AttributeKey[String]
|
||||||
|
|
||||||
def putUserName(serverSession:ServerSession, userName:String):Unit =
|
def putUserName(serverSession:ServerSession, userName:String):Unit =
|
||||||
serverSession.setAttribute(userNameSessionKey, userName)
|
serverSession.setAttribute(userNameSessionKey, userName)
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package gitbucket.core.util
|
|||||||
import java.io._
|
import java.io._
|
||||||
import java.sql._
|
import java.sql._
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import javax.xml.stream.{XMLStreamConstants, XMLInputFactory, XMLOutputFactory}
|
|
||||||
import ControlUtil._
|
import ControlUtil._
|
||||||
import scala.StringBuilder
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import scala.collection.mutable
|
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,65 +61,34 @@ object JDBCUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def importAsXML(in: InputStream): Unit = {
|
def importAsSQL(in: InputStream): Unit = {
|
||||||
conn.setAutoCommit(false)
|
conn.setAutoCommit(false)
|
||||||
try {
|
try {
|
||||||
val factory = XMLInputFactory.newInstance()
|
using(in){ in =>
|
||||||
using(factory.createXMLStreamReader(in, "UTF-8")){ reader =>
|
var out = new ByteArrayOutputStream()
|
||||||
// stateful objects
|
|
||||||
var elementName = ""
|
|
||||||
var insertTable = ""
|
|
||||||
var insertColumns = Map.empty[String, (String, String)]
|
|
||||||
|
|
||||||
while(reader.hasNext){
|
var length = 0
|
||||||
reader.next()
|
val bytes = new scala.Array[Byte](1024 * 8)
|
||||||
|
var stringLiteral = false
|
||||||
|
|
||||||
reader.getEventType match {
|
var count = 0
|
||||||
case XMLStreamConstants.START_ELEMENT =>
|
|
||||||
elementName = reader.getName.getLocalPart
|
while({ length = in.read(bytes); length != -1 }){
|
||||||
if(elementName == "insert"){
|
for(i <- 0 to length - 1){
|
||||||
insertTable = reader.getAttributeValue(null, "table")
|
val c = bytes(i)
|
||||||
} else if(elementName == "delete"){
|
if(c == '\''){
|
||||||
val tableName = reader.getAttributeValue(null, "table")
|
stringLiteral = !stringLiteral
|
||||||
conn.update(s"DELETE FROM ${tableName}")
|
|
||||||
} else if(elementName == "column"){
|
|
||||||
val columnName = reader.getAttributeValue(null, "name")
|
|
||||||
val columnType = reader.getAttributeValue(null, "type")
|
|
||||||
val columnValue = reader.getElementText
|
|
||||||
insertColumns = insertColumns + (columnName -> (columnType, columnValue))
|
|
||||||
}
|
}
|
||||||
case XMLStreamConstants.END_ELEMENT =>
|
if(c == ';' && !stringLiteral){
|
||||||
// Execute insert statement
|
val sql = new String(out.toByteArray, "UTF-8")
|
||||||
reader.getName.getLocalPart match {
|
conn.update(sql)
|
||||||
case "insert" => {
|
out = new ByteArrayOutputStream()
|
||||||
val sb = new StringBuilder()
|
|
||||||
sb.append(s"INSERT INTO ${insertTable} (")
|
|
||||||
sb.append(insertColumns.map { case (columnName, _) => columnName }.mkString(", "))
|
|
||||||
sb.append(") VALUES (")
|
|
||||||
sb.append(insertColumns.map { case (_, (columnType, columnValue)) =>
|
|
||||||
if(columnType == null || columnValue == null){
|
|
||||||
"NULL"
|
|
||||||
} else if(columnType == "string"){
|
|
||||||
"'" + columnValue.replace("'", "''") + "'"
|
|
||||||
} else if(columnType == "timestamp"){
|
|
||||||
"'" + columnValue + "'"
|
|
||||||
} else {
|
} else {
|
||||||
columnValue.toString
|
out.write(c)
|
||||||
}
|
}
|
||||||
}.mkString(", "))
|
|
||||||
sb.append(")")
|
|
||||||
|
|
||||||
conn.update(sb.toString)
|
|
||||||
|
|
||||||
insertColumns = Map.empty[String, (String, String)] // Clear column information
|
|
||||||
}
|
|
||||||
case _ => // Nothing to do
|
|
||||||
}
|
|
||||||
case _ => // Nothing to do
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
@@ -133,68 +99,6 @@ object JDBCUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def exportAsXML(targetTables: Seq[String]): File = {
|
|
||||||
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
|
||||||
val file = File.createTempFile("gitbucket-export-", ".xml")
|
|
||||||
|
|
||||||
val factory = XMLOutputFactory.newInstance()
|
|
||||||
using(factory.createXMLStreamWriter(new FileOutputStream(file), "UTF-8")){ writer =>
|
|
||||||
val dbMeta = conn.getMetaData
|
|
||||||
val allTablesInDatabase = allTablesOrderByDependencies(dbMeta)
|
|
||||||
|
|
||||||
writer.writeStartDocument("UTF-8", "1.0")
|
|
||||||
writer.writeStartElement("tables")
|
|
||||||
|
|
||||||
println(allTablesInDatabase.mkString(", "))
|
|
||||||
|
|
||||||
allTablesInDatabase.reverse.foreach { tableName =>
|
|
||||||
if (targetTables.contains(tableName)) {
|
|
||||||
writer.writeStartElement("delete")
|
|
||||||
writer.writeAttribute("table", tableName)
|
|
||||||
writer.writeEndElement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allTablesInDatabase.foreach { tableName =>
|
|
||||||
if (targetTables.contains(tableName)) {
|
|
||||||
select(s"SELECT * FROM ${tableName}") { rs =>
|
|
||||||
writer.writeStartElement("insert")
|
|
||||||
writer.writeAttribute("table", tableName)
|
|
||||||
val rsMeta = rs.getMetaData
|
|
||||||
(1 to rsMeta.getColumnCount).foreach { i =>
|
|
||||||
val columnName = rsMeta.getColumnName(i)
|
|
||||||
val (columnType, columnValue) = if(rs.getObject(columnName) == null){
|
|
||||||
(null, null)
|
|
||||||
} else {
|
|
||||||
rsMeta.getColumnType(i) match {
|
|
||||||
case Types.BOOLEAN | Types.BIT => ("boolean", rs.getBoolean(columnName))
|
|
||||||
case Types.VARCHAR | Types.CLOB | Types.CHAR | Types.LONGVARCHAR => ("string", rs.getString(columnName))
|
|
||||||
case Types.INTEGER => ("int", rs.getInt(columnName))
|
|
||||||
case Types.TIMESTAMP => ("timestamp", dateFormat.format(rs.getTimestamp(columnName)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.writeStartElement("column")
|
|
||||||
writer.writeAttribute("name", columnName)
|
|
||||||
if(columnType != null){
|
|
||||||
writer.writeAttribute("type", columnType)
|
|
||||||
}
|
|
||||||
if(columnValue != null){
|
|
||||||
writer.writeCharacters(columnValue.toString)
|
|
||||||
}
|
|
||||||
writer.writeEndElement()
|
|
||||||
}
|
|
||||||
writer.writeEndElement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.writeEndElement()
|
|
||||||
writer.writeEndDocument()
|
|
||||||
}
|
|
||||||
|
|
||||||
file
|
|
||||||
}
|
|
||||||
|
|
||||||
def exportAsSQL(targetTables: Seq[String]): File = {
|
def exportAsSQL(targetTables: Seq[String]): File = {
|
||||||
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val file = File.createTempFile("gitbucket-export-", ".sql")
|
val file = File.createTempFile("gitbucket-export-", ".sql")
|
||||||
|
|||||||
@@ -13,22 +13,12 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<input type="submit" class="btn btn-success pull-right" value="Export">
|
<input type="submit" class="btn btn-success pull-right" value="Export">
|
||||||
<div class="radio pull-right" style="margin-right: 10px;">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="type" value="sql">SQL
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio pull-right" style="margin-right: 10px;">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="type" value="xml" checked>XML
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong">Import (only XML)</div>
|
<div class="panel-heading strong">Import</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form class="form form-horizontal" action="@context.path/upload/import" method="POST" enctype="multipart/form-data" id="import-form">
|
<form class="form form-horizontal" action="@context.path/upload/import" method="POST" enctype="multipart/form-data" id="import-form">
|
||||||
<input type="file" name="file" id="file">
|
<input type="file" name="file" id="file">
|
||||||
@@ -42,10 +32,10 @@
|
|||||||
$(function(){
|
$(function(){
|
||||||
$('#import-form').submit(function(){
|
$('#import-form').submit(function(){
|
||||||
if($('#file').val() == ''){
|
if($('#file').val() == ''){
|
||||||
alert('Choose an import XML file.');
|
alert('Choose an import SQL file.');
|
||||||
return false;
|
return false;
|
||||||
} else if(!$('#file').val().endsWith(".xml")){
|
} else if(!$('#file').val().endsWith(".sql")){
|
||||||
alert('Import is available for only the XML file.');
|
alert('Import is available for only the SQL file.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return confirm('All existing data is deleted before importing.\nAre you sure?');
|
return confirm('All existing data is deleted before importing.\nAre you sure?');
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
@(plugins: List[(gitbucket.core.plugin.PluginInfo, String)])(implicit context: gitbucket.core.controller.Context)
|
@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
|
||||||
@gitbucket.core.html.main("Plugins"){
|
@gitbucket.core.html.main("Plugins"){
|
||||||
@gitbucket.core.admin.html.menu("plugins") {
|
@gitbucket.core.admin.html.menu("plugins") {
|
||||||
<h1>Installed plugins</h1>
|
<h1>Installed plugins</h1>
|
||||||
|
|
||||||
@if(plugins.size > 0) {
|
@if(plugins.size > 0) {
|
||||||
<ul>
|
<ul>
|
||||||
@plugins.map { case (plugin, migrationVersion) =>
|
@plugins.map { plugin =>
|
||||||
<li><a href="#@plugin.pluginId">@plugin.pluginId:@migrationVersion</a></li>
|
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@plugins.map { case (plugin, migrationVersion) =>
|
@plugins.map { plugin =>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong">@plugin.pluginName</div>
|
<div class="panel-heading strong">@plugin.pluginName</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label class="col-md-2">Version</label>
|
<label class="col-md-2">Version</label>
|
||||||
<span class="col-md-10">@migrationVersion @if(plugin.pluginVersion != migrationVersion){ <span class="error">(Migration is failed, installed version is @plugin.pluginVersion)</span> }</span>
|
<span class="col-md-10">@plugin.pluginVersion</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label class="col-md-2">Name</label>
|
<label class="col-md-2">Name</label>
|
||||||
|
|||||||
7
src/main/twirl/gitbucket/core/goget.scala.html
Normal file
7
src/main/twirl/gitbucket/core/goget.scala.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
@gitbucket.core.helper.html.attached(
|
@gitbucket.core.helper.html.attached(
|
||||||
repository = repository,
|
repository = repository,
|
||||||
completionContext = completionContext,
|
completionContext = completionContext,
|
||||||
generateScript = enableWikiLink
|
generateScript = !enableWikiLink
|
||||||
)(textarea)
|
)(textarea)
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="tab@(uid+1)">
|
<div class="tab-pane" id="tab@(uid+1)">
|
||||||
|
|||||||
@@ -34,10 +34,8 @@
|
|||||||
<script src="@helpers.assets/vendors/jquery-hotkeys/jquery.hotkeys.js"></script>
|
<script src="@helpers.assets/vendors/jquery-hotkeys/jquery.hotkeys.js"></script>
|
||||||
<script src="@helpers.assets/vendors/jquery-textcomplete-1.6.2/jquery.textcomplete.js"></script>
|
<script src="@helpers.assets/vendors/jquery-textcomplete-1.6.2/jquery.textcomplete.js"></script>
|
||||||
@repository.map { repository =>
|
@repository.map { repository =>
|
||||||
@if(!repository.repository.isPrivate){
|
|
||||||
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
|
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
|
||||||
}
|
}
|
||||||
}
|
|
||||||
<script src="@helpers.assets/vendors/AdminLTE-2.2.3/js/app.js" type="text/javascript"></script>
|
<script src="@helpers.assets/vendors/AdminLTE-2.2.3/js/app.js" type="text/javascript"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="skin-blue">
|
<body class="skin-blue">
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ $(window).load(function(){
|
|||||||
var mode = m[1];
|
var mode = m[1];
|
||||||
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1] == 'blame' ? '/blob' : '/blame') + m[2]);
|
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1] == 'blame' ? '/blob' : '/blame') + m[2]);
|
||||||
if(pre.parents("div.box-content-bottom").find(".blame").length){
|
if(pre.parents("div.box-content-bottom").find(".blame").length){
|
||||||
pre.parents("div.container").toggleClass("blame-container", mode == 'blame');
|
pre.parent().toggleClass("blame-container", mode == 'blame');
|
||||||
updateSourceLineNum();
|
updateSourceLineNum();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ $(window).load(function(){
|
|||||||
$(document.body).toggleClass('no-box-shadow', document.body.style.boxShadow===undefined);
|
$(document.body).toggleClass('no-box-shadow', document.body.style.boxShadow===undefined);
|
||||||
$('.blame-action').addClass("active");
|
$('.blame-action').addClass("active");
|
||||||
var base = $('<div class="blame">').css({height: pre.height()}).prependTo(pre.parents("div.box-content-bottom"));
|
var base = $('<div class="blame">').css({height: pre.height()}).prependTo(pre.parents("div.box-content-bottom"));
|
||||||
base.parents("div.container").addClass("blame-container");
|
base.parent().addClass("blame-container");
|
||||||
updateSourceLineNum();
|
updateSourceLineNum();
|
||||||
$.get($('.blame-action').data('url')).done(function(data){
|
$.get($('.blame-action').data('url')).done(function(data){
|
||||||
var blame = data.blame;
|
var blame = data.blame;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered table-hover">
|
||||||
@commits.map { day =>
|
@commits.map { day =>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="@day.size" width="100">@helpers.date(day.head.commitTime)</th>
|
<th rowspan="@day.size" width="100">@helpers.date(day.head.commitTime)</th>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package gitbucket.core.ssh
|
package gitbucket.core.ssh
|
||||||
|
|
||||||
import org.apache.sshd.server.command.UnknownCommand
|
import org.apache.sshd.server.scp.UnknownCommand
|
||||||
import org.scalatest.FunSpec
|
import org.scalatest.FunSpec
|
||||||
|
|
||||||
class GitCommandFactorySpec extends FunSpec {
|
class GitCommandFactorySpec extends FunSpec {
|
||||||
|
|||||||
Reference in New Issue
Block a user