diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e043fc..953878b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ timeout-minutes: 30 strategy: matrix: - java: [8, 11] + java: [8, 11, 17] steps: - uses: actions/checkout@v2 - name: Cache diff --git a/build.sbt b/build.sbt index fcb3c0d..ee12844 100644 --- a/build.sbt +++ b/build.sbt @@ -4,9 +4,9 @@ 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 @@ organization := Organization name := Name version := GitBucketVersion -scalaVersion := "2.13.6" +scalaVersion := "2.13.7" scalafmtOnCompile := true @@ -28,8 +28,6 @@ "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 @@ "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 ~= { diff --git a/project/plugins.sbt b/project/plugins.sbt index f7fd644..26b3fff 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -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 diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java index 16888b8..4d98af0 100644 --- a/src/main/java/JettyLauncher.java +++ b/src/main/java/JettyLauncher.java @@ -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 @@ 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 @@ 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 connectorsSet = Stream.of(fallback(connectors, Defaults.CONNECTORS) + .toLowerCase().split(",")).map(String::trim).collect(toSet()); + + final List 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 @@ 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 @@ 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 @@ } } - private static int getPort(String port){ - if(port == null) { - return 8080; - } else { - return Integer.parseInt(port); + private static T fallback(R value, T defaultValue, Function converter) { + return value == null ? defaultValue : converter.apply(value); + } + + private static 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 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); } } diff --git a/src/main/resources/bundle-plugins.txt b/src/main/resources/bundle-plugins.txt index c14b6dc..f90a388 100644 --- a/src/main/resources/bundle-plugins.txt +++ b/src/main/resources/bundle-plugins.txt @@ -1,4 +1,4 @@ notifications:1.10.0 gist:4.21.0 emoji:4.6.0 -pages:1.9.0 +pages:1.10.0 diff --git a/src/main/resources/update/gitbucket-core_4.37.xml b/src/main/resources/update/gitbucket-core_4.37.xml new file mode 100644 index 0000000..c7609a6 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.37.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 4edc140..6306e9f 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -119,5 +119,6 @@ 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")), ) diff --git a/src/main/scala/gitbucket/core/api/ApiIssue.scala b/src/main/scala/gitbucket/core/api/ApiIssue.scala index 8ab694e..a686cd4 100644 --- a/src/main/scala/gitbucket/core/api/ApiIssue.scala +++ b/src/main/scala/gitbucket/core/api/ApiIssue.scala @@ -17,7 +17,8 @@ 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 @@ 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 @@ user = user, assignee = assignee, labels = labels, + milestone = milestone, state = if (issue.closed) { "closed" } else { "open" }, body = issue.content.getOrElse(""), created_at = issue.registeredDate, diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 62a9716..ca04d8a 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -86,7 +86,7 @@ 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 @@ )(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( diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 8264ea3..c8c1377 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -197,7 +197,7 @@ 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 @@ 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( diff --git a/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala index b1eaf5f..350ad44 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala @@ -47,7 +47,8 @@ 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 @@ 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 @@ 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() diff --git a/src/main/scala/gitbucket/core/controller/api/ApiIssueMilestoneControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiIssueMilestoneControllerBase.scala index ec55ac0..246520f 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiIssueMilestoneControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiIssueMilestoneControllerBase.scala @@ -102,17 +102,4 @@ 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 - ) - ) - } } diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index af0cec6..f701eb8 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -341,8 +341,11 @@ } 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 @@ 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 @@ 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"), diff --git a/src/main/scala/gitbucket/core/service/MilestonesService.scala b/src/main/scala/gitbucket/core/service/MilestonesService.scala index f5f9849..9bff3cc 100644 --- a/src/main/scala/gitbucket/core/service/MilestonesService.scala +++ b/src/main/scala/gitbucket/core/service/MilestonesService.scala @@ -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 @@ .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 + ) + ) + } } diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index 0986fe8..f064597 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -35,6 +35,7 @@ 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 @@ 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 @@ 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 @@ repositoryUser = repoOwner, assignedUser = assignedUser, sender = sender, - labels = labels + labels = labels, + milestone = milestone ) } } @@ -760,7 +764,8 @@ repositoryUser: Account, assignedUser: Option[Account], sender: Account, - labels: List[Label] + labels: List[Label], + milestone: Option[ApiMilestone] ): WebHookIssueCommentPayload = WebHookIssueCommentPayload( action = "created", @@ -770,7 +775,8 @@ 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), diff --git a/src/main/scala/gitbucket/core/service/WikiService.scala b/src/main/scala/gitbucket/core/service/WikiService.scala index 5cd4306..35cc065 100644 --- a/src/main/scala/gitbucket/core/service/WikiService.scala +++ b/src/main/scala/gitbucket/core/service/WikiService.scala @@ -75,13 +75,15 @@ 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 diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index c2ebcfd..3e30d6d 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -5,9 +5,9 @@ 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 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 @@ 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 @@ } } - 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 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") diff --git a/src/main/scala/gitbucket/core/ssh/NoShell.scala b/src/main/scala/gitbucket/core/ssh/NoShell.scala index 4b00a15..a3e0dc9 100644 --- a/src/main/scala/gitbucket/core/ssh/NoShell.scala +++ b/src/main/scala/gitbucket/core/ssh/NoShell.scala @@ -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 @@ callback.onExit(127) } - override def destroy(): Unit = {} + override def destroy(channel: ChannelSession): Unit = {} override def setInputStream(in: InputStream): Unit = { this.in = in diff --git a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala index 2f79f4e..4501e71 100644 --- a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -8,13 +8,13 @@ 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) diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index ad17a9e..9e2d81e 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -382,7 +382,7 @@ 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 @@ */ 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 @@ 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] = { diff --git a/src/main/twirl/gitbucket/core/helper/preview.scala.html b/src/main/twirl/gitbucket/core/helper/preview.scala.html index aa18dea..1276c68 100644 --- a/src/main/twirl/gitbucket/core/helper/preview.scala.html +++ b/src/main/twirl/gitbucket/core/helper/preview.scala.html @@ -40,8 +40,6 @@ - -