Merge branch 'master' into custom-ssh-url

This commit is contained in:
Naoki Takezoe
2021-11-28 14:56:55 +09:00
committed by GitHub
29 changed files with 351 additions and 191 deletions

View File

@@ -8,7 +8,7 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
java: [8, 11]
java: [8, 11, 17]
steps:
- uses: actions/checkout@v2
- name: Cache

View File

@@ -4,9 +4,9 @@ import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.36.2"
val ScalatraVersion = "2.8.0"
val JettyVersion = "9.4.43.v20210629"
val JgitVersion = "5.12.0.202106070339-r"
val ScalatraVersion = "2.8.2"
val JettyVersion = "9.4.44.v20210927"
val JgitVersion = "5.13.0.202109080827-r"
lazy val root = (project in file("."))
.enablePlugins(SbtTwirl, ScalatraPlugin)
@@ -15,7 +15,7 @@ sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.13.6"
scalaVersion := "2.13.7"
scalafmtOnCompile := true
@@ -28,8 +28,6 @@ resolvers ++= Seq(
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
)
libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % "always"
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % JgitVersion,
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % JgitVersion,
@@ -44,34 +42,34 @@ libraryDependencies ++= Seq(
"org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.8.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
"org.apache.sshd" % "apache-sshd" % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.sshd" % "apache-sshd" % "2.7.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "2.1.0",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4",
"org.postgresql" % "postgresql" % "42.2.23",
"ch.qos.logback" % "logback-classic" % "1.2.5",
"org.postgresql" % "postgresql" % "42.3.1",
"ch.qos.logback" % "logback-classic" % "1.2.7",
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.1",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.10",
"io.github.java-diff-utils" % "java-diff-utils" % "4.11",
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.14",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.15",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.20",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.13.2" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
"org.mockito" % "mockito-core" % "3.12.4" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.39.6" % "test",
"org.testcontainers" % "mysql" % "1.16.0" % "test",
"org.testcontainers" % "postgresql" % "1.16.0" % "test",
"org.mockito" % "mockito-core" % "4.1.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.39.12" % "test",
"org.testcontainers" % "mysql" % "1.16.2" % "test",
"org.testcontainers" % "postgresql" % "1.16.2" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.3.0",
"org.kohsuke" % "github-api" % "1.132" % "test"
"org.kohsuke" % "github-api" % "1.301" % "test"
)
libraryDependencies ~= {

View File

@@ -1,11 +1,11 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.4")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.0.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.8.2")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.2")
addDependencyTreePlugin

View File

@@ -1,30 +1,66 @@
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.SecuredRedirectHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.server.session.DefaultSessionCache;
import org.eclipse.jetty.server.session.FileSessionDataStore;
import org.eclipse.jetty.server.session.SessionCache;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
import java.net.InetAddress;
import java.net.URL;
import java.net.InetSocketAddress;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toSet;
public class JettyLauncher {
private interface Defaults {
String CONNECTORS = "http";
String HOST = "0.0.0.0";
int HTTP_PORT = 8080;
int HTTPS_PORT = 8443;
boolean REDIRECT_HTTPS = false;
}
private interface Connectors {
String HTTP = "http";
String HTTPS = "https";
}
public static void main(String[] args) throws Exception {
System.setProperty("java.awt.headless", "true");
String connectors = getEnvironmentVariable("gitbucket.connectors");
String host = getEnvironmentVariable("gitbucket.host");
String port = getEnvironmentVariable("gitbucket.port");
InetSocketAddress address;
String securePort = getEnvironmentVariable("gitbucket.securePort");
String keyStorePath = getEnvironmentVariable("gitbucket.keyStorePath");
String keyStorePassword = getEnvironmentVariable("gitbucket.keyStorePassword");
String keyManagerPassword = getEnvironmentVariable("gitbucket.keyManagerPassword");
String redirectHttps = getEnvironmentVariable("gitbucket.redirectHttps");
String contextPath = getEnvironmentVariable("gitbucket.prefix");
String tmpDirPath = getEnvironmentVariable("gitbucket.tempDir");
boolean forceHttps = false;
boolean saveSessions = false;
for(String arg: args) {
@@ -32,15 +68,33 @@ public class JettyLauncher {
saveSessions = true;
}
if(arg.startsWith("--") && arg.contains("=")) {
String[] dim = arg.split("=");
if(dim.length >= 2) {
String[] dim = arg.split("=", 2);
if(dim.length == 2) {
switch (dim[0]) {
case "--connectors":
connectors = dim[1];
break;
case "--host":
host = dim[1];
break;
case "--port":
port = dim[1];
break;
case "--secure_port":
securePort = dim[1];
break;
case "--key_store_path":
keyStorePath = dim[1];
break;
case "--key_store_password":
keyStorePassword = dim[1];
break;
case "--key_manager_password":
keyManagerPassword = dim[1];
break;
case "--redirect_https":
redirectHttps = dim[1];
break;
case "--prefix":
contextPath = dim[1];
break;
@@ -62,38 +116,69 @@ public class JettyLauncher {
contextPath = "/" + contextPath;
}
if(host != null) {
address = new InetSocketAddress(host, getPort(port));
} else {
address = new InetSocketAddress(getPort(port));
final String hostName = InetAddress.getByName(fallback(host, Defaults.HOST)).getHostName();
final Server server = new Server();
final Set<String> connectorsSet = Stream.of(fallback(connectors, Defaults.CONNECTORS)
.toLowerCase().split(",")).map(String::trim).collect(toSet());
final List<ServerConnector> connectorInstances = new ArrayList<>();
final HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSendServerVersion(false);
if (connectorsSet.contains(Connectors.HTTPS)) {
httpConfig.setSecurePort(fallback(securePort, Defaults.HTTPS_PORT, Integer::parseInt));
}
Server server = new Server(address);
if (connectorsSet.contains(Connectors.HTTP)) {
final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
connector.setHost(hostName);
connector.setPort(fallback(port, Defaults.HTTP_PORT, Integer::parseInt));
// SelectChannelConnector connector = new SelectChannelConnector();
// if(host != null) {
// connector.setHost(host);
// }
// connector.setMaxIdleTime(1000 * 60 * 60);
// connector.setSoLingerTime(-1);
// connector.setPort(port);
// server.addConnector(connector);
// Disabling Server header
for (Connector connector : server.getConnectors()) {
for (ConnectionFactory factory : connector.getConnectionFactories()) {
if (factory instanceof HttpConnectionFactory) {
((HttpConnectionFactory) factory).getHttpConfiguration().setSendServerVersion(false);
}
}
connectorInstances.add(connector);
}
if (connectorsSet.contains(Connectors.HTTPS)) {
final SslContextFactory sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(requireNonNull(keyStorePath,
"You must specify a path to an SSL keystore via the --key_store_path command line argument" +
" or GITBUCKET_KEYSTOREPATH environment variable."));
sslContextFactory.setKeyStorePassword(requireNonNull(keyStorePassword,
"You must specify a an SSL keystore password via the --key_store_password argument" +
" or GITBUCKET_KEYSTOREPASSWORD environment variable."));
sslContextFactory.setKeyManagerPassword(requireNonNull(keyManagerPassword,
"You must specify a key manager password via the --key_manager_password' argument" +
" or GITBUCKET_KEYMANAGERPASSWORD environment variable."));
final HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
final ServerConnector connector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
new HttpConnectionFactory(httpsConfig));
connector.setHost(hostName);
connector.setPort(fallback(securePort, Defaults.HTTPS_PORT, Integer::parseInt));
connectorInstances.add(connector);
}
require(!connectorInstances.isEmpty(),
"No server connectors could be configured, please check your --connectors command line argument" +
" or GITBUCKET_CONNECTORS environment variable.");
server.setConnectors(connectorInstances.toArray(new ServerConnector[0]));
WebAppContext context = new WebAppContext();
if(saveSessions) {
File sessDir = new File(getGitBucketHome(), "sessions");
if(!sessDir.exists()){
sessDir.mkdirs();
mkdir(sessDir);
}
SessionHandler sessions = context.getSessionHandler();
SessionCache cache = new DefaultSessionCache(sessions);
@@ -107,7 +192,7 @@ public class JettyLauncher {
if(tmpDirPath == null || tmpDirPath.equals("")){
tmpDir = new File(getGitBucketHome(), "tmp");
if(!tmpDir.exists()){
tmpDir.mkdirs();
mkdir(tmpDir);
}
} else {
tmpDir = new File(tmpDirPath);
@@ -131,13 +216,16 @@ public class JettyLauncher {
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server);
context.setWar(location.toExternalForm());
if (forceHttps) {
context.setInitParameter("org.scalatra.ForceHttps", "true");
final HandlerList handlers = new HandlerList();
if (fallback(redirectHttps, Defaults.REDIRECT_HTTPS, Boolean::parseBoolean)) {
handlers.addHandler(new SecuredRedirectHandler());
}
Handler handler = addStatisticsHandler(context);
handlers.addHandler(addStatisticsHandler(context));
server.setHandler(handler);
server.setHandler(handlers);
server.setStopAtShutdown(true);
server.setStopTimeout(7_000);
server.start();
@@ -165,11 +253,28 @@ public class JettyLauncher {
}
}
private static int getPort(String port){
if(port == null) {
return 8080;
} else {
return Integer.parseInt(port);
private static <T, R> T fallback(R value, T defaultValue, Function<R, T> converter) {
return value == null ? defaultValue : converter.apply(value);
}
private static <T> T fallback(T value, T defaultValue) {
return fallback(value, defaultValue, identity());
}
private static void require(boolean condition, String message) {
if (!condition) {
throw new IllegalArgumentException(message);
}
}
private static <T> T requireNonNull(T value, String message) {
require(value != null, message);
return value;
}
private static void mkdir(File dir) {
if (!dir.mkdirs()) {
throw new RuntimeException("Unable to create directory: " + dir);
}
}

View File

@@ -1,4 +1,4 @@
notifications:1.10.0
gist:4.21.0
emoji:4.6.0
pages:1.9.0
pages:1.10.0

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT"/>
<modifyDataType columnName="URL" newDataType="varchar(400)" tableName="WEB_HOOK_EVENT"/>
<modifyDataType columnName="URL" newDataType="varchar(400)" tableName="WEB_HOOK"/>
<addForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, URL" referencedTableName="WEB_HOOK" referencedColumnNames="USER_NAME, REPOSITORY_NAME, URL" onDelete="CASCADE" onUpdate="CASCADE"/>
</changeSet>

View File

@@ -119,5 +119,6 @@ object GitBucketCoreModule
new Version("4.35.3"),
new Version("4.36.0", new LiquibaseMigration("update/gitbucket-core_4.36.xml")),
new Version("4.36.1"),
new Version("4.36.2")
new Version("4.36.2"),
new Version("4.37.0", new LiquibaseMigration("update/gitbucket-core_4.37.xml")),
)

View File

@@ -17,7 +17,8 @@ case class ApiIssue(
state: String,
created_at: Date,
updated_at: Date,
body: String
body: String,
milestone: Option[ApiMilestone]
)(repositoryName: RepositoryName, isPullRequest: Boolean) {
val id = 0 // dummy id
val assignees = List(assignee).flatten
@@ -43,7 +44,8 @@ object ApiIssue {
repositoryName: RepositoryName,
user: ApiUser,
assignee: Option[ApiUser],
labels: List[ApiLabel]
labels: List[ApiLabel],
milestone: Option[ApiMilestone]
): ApiIssue =
ApiIssue(
number = issue.issueId,
@@ -51,6 +53,7 @@ object ApiIssue {
user = user,
assignee = assignee,
labels = labels,
milestone = milestone,
state = if (issue.closed) { "closed" } else { "open" },
body = issue.content.getOrElse(""),
created_at = issue.registeredDate,

View File

@@ -86,7 +86,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val newForm = mapping(
"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(40)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress()))),
"extraMailAddresses" -> list(
@@ -98,7 +98,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)(AccountNewForm.apply)
val editForm = mapping(
"password" -> trim(label("Password", optional(text(maxlength(20))))),
"password" -> trim(label("Password", optional(text(maxlength(40))))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))),
"extraMailAddresses" -> list(

View File

@@ -197,7 +197,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val newUserForm = mapping(
"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(40)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress()))),
"extraMailAddresses" -> list(
@@ -211,7 +211,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val editUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier))),
"password" -> trim(label("Password", optional(text(maxlength(20))))),
"password" -> trim(label("Password", optional(text(maxlength(40))))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))),
"extraMailAddresses" -> list(

View File

@@ -47,7 +47,8 @@ trait ApiIssueControllerBase extends ControllerBase {
user = ApiUser(issueUser),
assignee = assignedUser.map(ApiUser(_)),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository)))
.map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
)
})
})
@@ -69,7 +70,8 @@ trait ApiIssueControllerBase extends ControllerBase {
RepositoryName(repository),
ApiUser(openedUser),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository)))
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
)
)
}) getOrElse NotFound()
@@ -103,7 +105,8 @@ trait ApiIssueControllerBase extends ControllerBase {
ApiUser(loginAccount),
issue.assignedUserName.flatMap(getAccountByUserName(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository)))
.map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
)
)
}) getOrElse NotFound()

View File

@@ -102,17 +102,4 @@ trait ApiIssueMilestoneControllerBase extends ControllerBase {
NoContent()
})
private def getApiMilestone(repository: RepositoryInfo, milestoneId: Int): Option[ApiMilestone] = {
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(p => p._1.milestoneId == milestoneId)
.map(
milestoneWithIssue =>
ApiMilestone(
repository.repository,
milestoneWithIssue._1,
milestoneWithIssue._2,
milestoneWithIssue._3
)
)
}
}

View File

@@ -341,8 +341,11 @@ trait IssuesService {
} else {
((t1.userName ++ "/" ++ t1.repositoryName) inSetBind (repos.map { case (owner, repo) => s"$owner/$repo" }))
}) &&
(t1.closed === (condition.state == "closed").bind)
.&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
(condition.state match {
case "open" => t1.closed === false
case "closed" => t1.closed === true
case _ => t1.closed === true || t1.closed === false
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
.&&(t1.priorityId.? isEmpty, condition.priority == Some(None))
.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
.&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
@@ -939,7 +942,7 @@ object IssuesService {
case x => Some(x)
},
param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "state", Seq("open", "closed", "all")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),
@@ -960,7 +963,7 @@ object IssuesService {
case x => Some(x)
},
param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "state", Seq("open", "closed", "all")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),

View File

@@ -1,9 +1,11 @@
package gitbucket.core.service
import gitbucket.core.api.ApiMilestone
import gitbucket.core.model.Milestone
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType
import gitbucket.core.service.RepositoryService.RepositoryInfo
trait MilestonesService {
@@ -73,4 +75,17 @@ trait MilestonesService {
.sortBy(t => (t.dueDate.asc, t.closedDate.desc, t.milestoneId.desc))
.list
def getApiMilestone(repository: RepositoryInfo, milestoneId: Int)(implicit s: Session): Option[ApiMilestone] = {
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(p => p._1.milestoneId == milestoneId)
.map(
milestoneWithIssue =>
ApiMilestone(
repository.repository,
milestoneWithIssue._1,
milestoneWithIssue._2,
milestoneWithIssue._3
)
)
}
}

View File

@@ -35,6 +35,7 @@ import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import gitbucket.core.model.WebHookContentType
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.view.helpers.getApiMilestone
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.entity.ContentType
@@ -394,7 +395,8 @@ trait WebHookPullRequestService extends WebHookService {
ApiUser(issueUser),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository)))
.map(ApiLabel(_, RepositoryName(repository))),
getApiMilestone(repository, issue.milestoneId getOrElse (0))
),
sender = ApiUser(sender)
)
@@ -576,6 +578,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
commenter <- users.get(issueComment.commentedUserName)
assignedUser = issue.assignedUserName.flatMap(users.get(_))
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
} yield {
WebHookIssueCommentPayload(
issue = issue,
@@ -586,7 +589,8 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
repositoryUser = repoOwner,
assignedUser = assignedUser,
sender = sender,
labels = labels
labels = labels,
milestone = milestone
)
}
}
@@ -760,7 +764,8 @@ object WebHookService {
repositoryUser: Account,
assignedUser: Option[Account],
sender: Account,
labels: List[Label]
labels: List[Label],
milestone: Option[ApiMilestone]
): WebHookIssueCommentPayload =
WebHookIssueCommentPayload(
action = "created",
@@ -770,7 +775,8 @@ object WebHookService {
RepositoryName(repository),
ApiUser(issueUser),
assignedUser.map(ApiUser(_)),
labels.map(ApiLabel(_, RepositoryName(repository)))
labels.map(ApiLabel(_, RepositoryName(repository))),
milestone
),
comment =
ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser), issue.isPullRequest),

View File

@@ -75,13 +75,15 @@ trait WikiService {
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
if (!JGitUtil.isEmpty(git)) {
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
val fileName = pageName + ".md"
JGitUtil.getLatestCommitFromPath(git, fileName, "master").map { latestCommit =>
val content = JGitUtil.getContentFromPath(git, latestCommit.getTree, fileName, true)
WikiPageInfo(
file.name,
StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
file.author,
file.time,
file.commitId
fileName,
StringUtil.convertFromByteArray(content.getOrElse(Array.empty)),
latestCommit.getAuthorIdent.getName,
latestCommit.getAuthorIdent.getWhen,
latestCommit.getName
)
}
} else None

View File

@@ -5,9 +5,9 @@ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService}
import gitbucket.core.servlet.{CommitLogHook, Database}
import gitbucket.core.util.Directory
import org.apache.sshd.server.{Environment, ExitCallback, SessionAware}
import org.apache.sshd.server.{Environment, ExitCallback}
import org.apache.sshd.server.command.{Command, CommandFactory}
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.server.session.{ServerSession, ServerSessionAware}
import org.slf4j.LoggerFactory
import java.io.{File, InputStream, OutputStream}
@@ -15,6 +15,7 @@ import org.eclipse.jgit.api.Git
import Directory._
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
import org.apache.sshd.server.channel.ChannelSession
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.shell.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
@@ -28,7 +29,7 @@ object GitCommand {
val SimpleCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?(.+\.git)'\Z""".r
}
abstract class GitCommand extends Command with SessionAware {
abstract class GitCommand extends Command with ServerSessionAware {
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
@@ -61,12 +62,12 @@ abstract class GitCommand extends Command with SessionAware {
}
}
final override def start(env: Environment): Unit = {
final override def start(channel: ChannelSession, env: Environment): Unit = {
val thread = new Thread(newTask())
thread.start()
}
override def destroy(): Unit = {}
override def destroy(channel: ChannelSession): Unit = {}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
@@ -235,7 +236,7 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting)
class GitCommandFactory(baseUrl: String, sshAddress: SshAddress) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
override def createCommand(channel: ChannelSession, command: String): Command = {
import GitCommand._
logger.debug(s"command: $command")

View File

@@ -1,20 +1,22 @@
package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.apache.sshd.common.Factory
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.{Environment, ExitCallback}
import org.apache.sshd.server.command.Command
import java.io.{OutputStream, InputStream}
import org.apache.sshd.server.shell.ShellFactory
import java.io.{InputStream, OutputStream}
import org.eclipse.jgit.lib.Constants
class NoShell(sshAddress: SshAddress) extends Factory[Command] {
override def create(): Command = new Command() {
class NoShell(sshAddress: SshAddress) extends ShellFactory {
override def createShell(channel: ChannelSession): Command = new Command() {
private var in: InputStream = null
private var out: OutputStream = null
private var err: OutputStream = null
private var callback: ExitCallback = null
override def start(env: Environment): Unit = {
override def start(channel: ChannelSession, env: Environment): Unit = {
val placeholderAddress = sshAddress.getUrl("OWNER", "REPOSITORY_NAME")
val message =
"""
@@ -41,7 +43,7 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
callback.onExit(127)
}
override def destroy(): Unit = {}
override def destroy(channel: ChannelSession): Unit = {}
override def setInputStream(in: InputStream): Unit = {
this.in = in

View File

@@ -8,13 +8,13 @@ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.common.AttributeStore
import org.apache.sshd.common.AttributeRepository
import org.slf4j.LoggerFactory
object PublicKeyAuthenticator {
// put in the ServerSession here to be read by GitCommand later
private val authTypeSessionKey = new AttributeStore.AttributeKey[AuthType]
private val authTypeSessionKey = new AttributeRepository.AttributeKey[AuthType]
def putAuthType(serverSession: ServerSession, authType: AuthType): Unit =
serverSession.setAttribute(authTypeSessionKey, authType)

View File

@@ -382,7 +382,7 @@ object JGitUtil {
path: String = ".",
baseUrl: Option[String] = None,
commitCount: Int = 0,
maxFiles: Int = 100
maxFiles: Int = 5
): List[FileInfo] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val objectId = git.getRepository.resolve(revision)
@@ -658,9 +658,13 @@ object JGitUtil {
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
paths.map { path =>
paths.flatMap { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
(path, commit)
if (commit == null) {
None
} else {
Some((path, commit))
}
}.toMap
}
@@ -671,11 +675,10 @@ object JGitUtil {
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
getDiffEntries(git, from, to)
.map { entry =>
.foreach { entry =>
df.format(entry)
new String(out.toByteArray, "UTF-8")
}
.mkString("\n")
new String(out.toByteArray, "UTF-8")
}
private def getDiffEntries(git: Git, from: Option[String], to: String): Seq[DiffEntry] = {

View File

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

View File

@@ -244,7 +244,7 @@ function updateHighlighting() {
const isDark = @{highlighterTheme.contains("dark").toString};
if (hash.match(/#L\d+(-L\d+)?/)) {
if (isDark) {
$('li.highlight').removeClass('highlight-dark');
$('li.highlight-dark').removeClass('highlight-dark');
} else {
$('li.highlight').removeClass('highlight');
}

View File

@@ -9,32 +9,22 @@
@gitbucket.core.html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.helper.html.information(info)
@gitbucket.core.html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width">
<li>
<h1 class="wiki-title"><span class="muted">Compare Revisions</span></h1>
</li>
<li class="pull-right">
<div class="btn-group">
@if(pageName.isDefined){
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)">View Page</a>
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_history">Back to Page History</a>
<div class="pull-right">
@if(pageName.isDefined){
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)">View Page</a>
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_history">Back to Page History</a>
} else {
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/_history">Back to Wiki History</a>
}
@if(isEditable) {
@if(pageName.isDefined) {
<a href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_revert/@from...@to" class="btn btn-danger">Revert Changes</a>
} else {
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/_history">Back to Wiki History</a>
<a href="@helpers.url(repository)/wiki/_revert/@from...@to" class="btn btn-danger">Revert Changes</a>
}
</div>
</li>
</ul>
<div class="pull-left">
@gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false)
}
</div>
@if(isEditable){
<div>
@if(pageName.isDefined){
<a href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a>
} else {
<a href="@helpers.url(repository)/wiki/_revert/@from...@to" class="btn">Revert Changes</a>
}
</div>
}
<h1 class="body-title"><span class="muted">Compare Revisions</span></h1>
@gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false)
}
}

View File

@@ -11,7 +11,7 @@
<a class="btn btn-danger" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_delete" id="delete">Delete Page</a>
}
</div>
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
<h1 class="body-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
<form action="@helpers.url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true" autocomplete="off">
<span id="error-pageName" class="error"></span>
<input type="text" name="pageName" value="@pageName" class="form-control" style="font-weight: bold; margin-bottom: 10px;" placeholder="Input a page name." aria-label="Page name"/>

View File

@@ -15,7 +15,7 @@
}
</div>
}
<h1 class="wiki-title">
<h1 class="body-title">
@if(pageName.isEmpty){
<span class="muted">History</span>
} else {

View File

@@ -4,20 +4,17 @@
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width">
<li>
<h1 class="wiki-title"><span class="muted">Pages</span></h1>
</li>
<li class="pull-right">
@if(isEditable){
<a class="btn btn-default" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
</li>
</ul>
<ul class="pull-left">
<div class="pull-right">
@if(isEditable){
<a class="btn btn-default" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
</div>
<h1 class="body-title"><span class="muted">Pages</span></h1>
<hr>
<ul>
@pages.map { page =>
<li><a href="@helpers.url(repository)/wiki/@helpers.urlEncode(page)">@page</a></li>
}
</ul>
}
}
}

View File

@@ -177,6 +177,16 @@ object ApiSpecModels {
updatedDate = date1
)
val milestone = Milestone(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
milestoneId = 1,
title = "Test milestone",
description = Some("Milestone description"),
dueDate = Some(date1),
closedDate = Some(date1)
)
// APIs
val apiUser = ApiUser(account)
@@ -193,12 +203,20 @@ object ApiSpecModels {
repositoryName = repo1Name
)
val apiMilestone = ApiMilestone(
repository = repository,
milestone = milestone,
open_issue_count = 1,
closed_issue_count = 1
)
val apiIssue = ApiIssue(
issue = issue,
repositoryName = repo1Name,
user = apiUser,
assignee = Some(apiUser),
labels = List(apiLabel)
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
val apiNotAssignedIssue = ApiIssue(
@@ -206,7 +224,8 @@ object ApiSpecModels {
repositoryName = repo1Name,
user = apiUser,
assignee = None,
labels = List(apiLabel)
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
val apiIssuePR = ApiIssue(
@@ -214,7 +233,8 @@ object ApiSpecModels {
repositoryName = repo1Name,
user = apiUser,
assignee = Some(apiUser),
labels = List(apiLabel)
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
val apiComment = ApiComment(
@@ -471,6 +491,19 @@ object ApiSpecModels {
val jsonLabel =
"""{"name":"bug","color":"f29513","url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/labels/bug"}"""
val jsonMilestone = """{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/milestones/1",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/milestone/1",
|"id":1,
|"number":1,
|"state":"closed",
|"title":"Test milestone",
|"description":"Milestone description",
|"open_issues":1,"closed_issues":1,
|"closed_at":"2011-04-14T16:00:49Z",
|"due_on":"2011-04-14T16:00:49Z"
|}""".stripMargin
val jsonIssue = s"""{
|"number":1347,
|"title":"Found a bug",
@@ -481,6 +514,7 @@ object ApiSpecModels {
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[$jsonUser],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
@@ -496,6 +530,7 @@ object ApiSpecModels {
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
@@ -512,6 +547,7 @@ object ApiSpecModels {
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"body":"Please pull these awesome changes",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[$jsonUser],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",

View File

@@ -121,7 +121,8 @@ class WebHookJsonFormatSpec extends AnyFunSuite {
repositoryUser = account,
assignedUser = Some(account),
sender = account,
labels = List(label)
labels = List(label),
milestone = Some(apiMilestone)
)
val expected = s"""{
|"action":"created",

View File

@@ -1,6 +1,7 @@
package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.shell.UnknownCommand
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
@@ -15,69 +16,70 @@ class GitCommandFactorySpec extends AnyWordSpec with Matchers {
}
"createCommand" when {
val channel = new ChannelSession()
"receiving a git-receive-pack command" should {
"return DefaultGitReceivePack" when {
"the path matches owner/repo" in new MockContext {
assert(factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack])
assert(factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack])
assert(factory.createCommand(channel, "git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack])
assert(factory.createCommand(channel, "git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack])
}
"the leading slash is left off and running on port 22" in new MockContext {
override val sshPort: Int = 22
assert(factory.createCommand("git-receive-pack 'owner/repo.git'").isInstanceOf[DefaultGitReceivePack])
assert(factory.createCommand("git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack])
assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.git'").isInstanceOf[DefaultGitReceivePack])
assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack])
}
}
"return UnknownCommand" when {
"the ssh port is not 22 and the leading slash is missing" in new MockContext {
override val sshPort: Int = 1337
assert(factory.createCommand("git-receive-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-receive-pack 'oranges.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-receive-pack 'apples.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack 'oranges.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack 'apples.git'").isInstanceOf[UnknownCommand])
}
"the path is malformed" in new MockContext {
assert(factory.createCommand("git-receive-pack '/owner/repo/wrong.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-receive-pack '/owner:repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-receive-pack '/oranges'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack '/owner/repo/wrong.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack '/owner:repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack '/oranges'").isInstanceOf[UnknownCommand])
}
}
}
"receiving a git-upload-pack command" should {
"return DefaultGitUploadPack" when {
"the path matches owner/repo" in new MockContext {
assert(factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[DefaultGitUploadPack])
assert(factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack])
assert(factory.createCommand(channel, "git-upload-pack '/owner/repo.git'").isInstanceOf[DefaultGitUploadPack])
assert(factory.createCommand(channel, "git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack])
}
"the leading slash is left off and running on port 22" in new MockContext {
override val sshPort = 22
assert(factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[DefaultGitUploadPack])
assert(factory.createCommand("git-upload-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack])
assert(factory.createCommand(channel, "git-upload-pack 'owner/repo.git'").isInstanceOf[DefaultGitUploadPack])
assert(factory.createCommand(channel, "git-upload-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack])
}
}
"return UnknownCommand" when {
"the ssh port is not 22 and the leading slash is missing" in new MockContext {
override val sshPort = 1337
assert(factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack 'oranges.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack 'apples.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-upload-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-upload-pack 'oranges.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-upload-pack 'apples.git'").isInstanceOf[UnknownCommand])
}
"the path is malformed" in new MockContext {
assert(factory.createCommand("git-upload-pack '/owner/repo'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack '/owner:repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack '/oranges'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-upload-pack '/owner/repo'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-upload-pack '/owner:repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-upload-pack '/oranges'").isInstanceOf[UnknownCommand])
}
}
}
"receiving any command not matching git-(receive|upload)-pack" should {
"return UnknownCommand" in new MockContext {
assert(factory.createCommand("git-destroy-pack '/owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-irrigate-pack '/apples.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-force-push '/stolen/nuke.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-delete '/backups.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-pack '/your/bags.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git- '/bananas.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("99 tickets of bugs on the wall").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-destroy-pack '/owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-irrigate-pack '/apples.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-force-push '/stolen/nuke.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-delete '/backups.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-pack '/your/bags.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git- '/bananas.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "99 tickets of bugs on the wall").isInstanceOf[UnknownCommand])
}
}
}