Compare commits

...

29 Commits

Author SHA1 Message Date
Naoki Takezoe
e7192655f7 Release 4.37.1 (#2949) 2021-12-14 01:05:27 +09:00
Naoki Takezoe
19ba09740c Update gist plugin and notification plugin for GitBucket 4.37.x (#2948) 2021-12-14 00:55:35 +09:00
Naoki Takezoe
d169777722 Fix SSHCommand extension point for apache-sshd 2.x (#2941) 2021-12-11 19:11:23 +09:00
Naoki Takezoe
ff8a5f6b77 Release 4.37.0 (#2940) 2021-12-11 14:28:23 +09:00
Scala Steward
ec953df156 Update sbt, sbt-dependency-tree to 1.5.6 2021-12-10 22:27:40 +09:00
Naoki Takezoe
d6a191d95b Enhance Git Reference APIs (#2937) 2021-12-06 17:16:33 +09:00
Naoki Takezoe
aba428bba1 Fix refs API as far as Jenkins github-branch-source plugin can detect tags (#2936)
Co-authored-by: Thomas Geier <thomas.geier@solidat.de>
2021-12-06 01:06:59 +09:00
Scala Steward
6ab37fd596 Update thumbnailator to 0.4.15 2021-12-05 16:11:17 +09:00
Scala Steward
73fc70f55b Update apache-sshd to 2.8.0 2021-12-04 08:27:01 +09:00
Scala Steward
aad18b7a50 Update sbt-scalafmt to 2.4.5 2021-12-04 08:26:37 +09:00
dependabot[bot]
cc278be5cd Bump actions/cache from 2.1.6 to 2.1.7
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.6 to 2.1.7.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.6...v2.1.7)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-30 09:24:19 +09:00
kenji yoshida
d0f4f82a0f pin jgit 5.x 2021-11-30 09:23:36 +09:00
Naoki Takezoe
1dcbf386b1 Fix SSH server handling (#2930) 2021-11-28 17:39:31 +09:00
Naoki Takezoe
414afd285c Remove unused imports 2021-11-28 15:14:51 +09:00
Naoki Takezoe
35b645d8b5 Merge branch 'keywordsalad-custom-ssh-url' 2021-11-28 15:08:04 +09:00
Naoki Takezoe
b3cba53866 Reformat GitCommandSpec 2021-11-28 15:02:29 +09:00
Naoki Takezoe
a4773bb3ca Merge branch 'master' into custom-ssh-url 2021-11-28 14:56:55 +09:00
Naoki Takezoe
863d8a4af5 Bump apache-sshd to 2.7.0 (#2929) 2021-11-28 14:47:58 +09:00
Scala Steward
3fccd7b53c Update oauth2-oidc-sdk to 9.20 2021-11-25 20:46:24 +09:00
Scala Steward
dd2760eaf7 Update github-api to 1.301 2021-11-24 12:10:25 +09:00
Scala Steward
824bafa739 Update github-api to 1.300 2021-11-22 11:15:11 +09:00
kaz-on
60cdaec05f Fix line highlighting in dark themes (#2921) 2021-11-22 01:31:52 +09:00
kaz-on
c204a435b3 Remove unnecessary loading of google-code-prettify (#2922) 2021-11-22 01:31:16 +09:00
Scala Steward
37accd92d6 Update mockito-core to 4.1.0 2021-11-20 07:35:42 +09:00
Scala Steward
01fd0ee1f0 Update sbt-scalafmt to 2.4.4 2021-11-19 07:19:22 +09:00
Scala Steward
fab1c74473 Update testcontainers-scala to 0.39.12 2021-11-15 06:14:54 +09:00
Scala Steward
0d8fcfd28d Update logback-classic to 1.2.7 2021-11-12 03:45:11 +09:00
Naoki Takezoe
b91a7c32a6 Relax max length limitation for WebHook URLs (#2915) 2021-11-11 01:39:12 +09:00
Logan McGrath
e7a6f0930b Closes #2818 - Supporting custom SSH URL's when hosting behind a proxy 2021-08-29 16:38:06 -07:00
44 changed files with 745 additions and 261 deletions

View File

@@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Cache - name: Cache
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.7
env: env:
cache-name: cache-sbt-libs cache-name: cache-sbt-libs
with: with:

View File

@@ -4,4 +4,5 @@ updates.includeScala = true
updates.pin = [ updates.pin = [
{ groupId = "org.eclipse.jetty", version = "9." } { groupId = "org.eclipse.jetty", version = "9." }
{ groupId = "org.eclipse.jgit", version = "5." }
] ]

View File

@@ -1,6 +1,19 @@
# Changelog # Changelog
All changes to the project will be documented in this file. All changes to the project will be documented in this file.
### 4.37.1 - 14 Dec 2021
- Update gist-plugin and notification-plugin
- Fix SSHCommand extension point for apache-sshd 2.x
### 4.37.0 - 11 Dec 2021
- Enhance Git Reference APIs
- Add milestone data to issue list API
- Support "all" in issue list API
- Support EDDSA in signed commit verification
- Support custom SSH url
- Relax max passward length limitation
- Relax max webhook url length limitation
### 4.36.2 - 16 Aug 2021 ### 4.36.2 - 16 Aug 2021
- Escape user name in avatar image tag - Escape user name in avatar image tag

View File

@@ -61,18 +61,19 @@ Support
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. - If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. - The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
What's New in 4.36.x What's New in 4.37.x
------------- -------------
### 4.36.2 - 16 Aug 2021 ### 4.37.1 - 14 Dec 2021
- Escape user name in avatar image tag - Update gist-plugin and notification-plugin
- Fix SSHCommand extension point for apache-sshd 2.x
### 4.36.1 - 22 Jul 2021 ### 4.37.0 - 11 Dec 2021
- Bump gitbucket-gist-plugin to 4.21.0 - Enhance Git Reference APIs
- Add milestone data to issue list API
### 4.36.0 - 17 Jul 2021 - Support "all" in issue list API
- Tag selector in the repository viewer - Support EDDSA in signed commit verification
- Link issues/pull requests of other repositories - Support custom SSH url
- Files and lines can be linked in the diff view - Relax max passward length limitation
- Option to disable XSS protection - Relax max webhook url length limitation
See the [change log](CHANGELOG.md) for all of the updates. See the [change log](CHANGELOG.md) for all of the updates.

View File

@@ -3,7 +3,7 @@ import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket" val Organization = "io.github.gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "4.36.2" val GitBucketVersion = "4.37.1"
val ScalatraVersion = "2.8.2" val ScalatraVersion = "2.8.2"
val JettyVersion = "9.4.44.v20210927" val JettyVersion = "9.4.44.v20210927"
val JgitVersion = "5.13.0.202109080827-r" val JgitVersion = "5.13.0.202109080827-r"
@@ -42,34 +42,34 @@ libraryDependencies ++= Seq(
"org.apache.commons" % "commons-email" % "1.5", "org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.8.0", "commons-net" % "commons-net" % "3.8.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13", "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.8.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", "org.apache.tika" % "tika-core" % "2.1.0",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13, "com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.199", "com.h2database" % "h2" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4", "org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4",
"org.postgresql" % "postgresql" % "42.3.1", "org.postgresql" % "postgresql" % "42.3.1",
"ch.qos.logback" % "logback-classic" % "1.2.6", "ch.qos.logback" % "logback-classic" % "1.2.7",
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"), "com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.1", "com.typesafe" % "config" % "1.4.1",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0", "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.11", "io.github.java-diff-utils" % "java-diff-utils" % "4.11",
"org.cache2k" % "cache2k-all" % "1.6.0.Final", "org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.14", "net.coobird" % "thumbnailator" % "0.4.15",
"com.github.zafarkhaja" % "java-semver" % "0.9.0", "com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.19", "com.nimbusds" % "oauth2-oidc-sdk" % "9.20",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.13.2" % "test", "junit" % "junit" % "4.13.2" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13, "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
"org.mockito" % "mockito-core" % "4.0.0" % "test", "org.mockito" % "mockito-core" % "4.1.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.39.11" % "test", "com.dimafeng" %% "testcontainers-scala" % "0.39.12" % "test",
"org.testcontainers" % "mysql" % "1.16.2" % "test", "org.testcontainers" % "mysql" % "1.16.2" % "test",
"org.testcontainers" % "postgresql" % "1.16.2" % "test", "org.testcontainers" % "postgresql" % "1.16.2" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0", "net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0", "is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.3.0", "org.ec4j.core" % "ec4j-core" % "0.3.0",
"org.kohsuke" % "github-api" % "1.135" % "test" "org.kohsuke" % "github-api" % "1.301" % "test"
) )
libraryDependencies ~= { libraryDependencies ~= {

View File

@@ -1 +1 @@
sbt.version=1.5.5 sbt.version=1.5.6

View File

@@ -1,6 +1,6 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.5")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1") addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4") addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")

View File

@@ -1,4 +1,4 @@
notifications:1.10.0 notifications:1.11.0
gist:4.21.0 gist:4.22.0
emoji:4.6.0 emoji:4.6.0
pages:1.10.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,7 @@ object GitBucketCoreModule
new Version("4.35.3"), new Version("4.35.3"),
new Version("4.36.0", new LiquibaseMigration("update/gitbucket-core_4.36.xml")), new Version("4.36.0", new LiquibaseMigration("update/gitbucket-core_4.36.xml")),
new Version("4.36.1"), 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")),
new Version("4.37.1")
) )

View File

@@ -1,5 +1,49 @@
package gitbucket.core.api package gitbucket.core.api
case class ApiObject(sha: String) import gitbucket.core.util.JGitUtil.TagInfo
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.lib.Ref
case class ApiRef(ref: String, `object`: ApiObject) case class ApiRefCommit(
sha: String,
`type`: String,
url: ApiPath
)
case class ApiRef(
ref: String,
node_id: String = "",
url: ApiPath,
`object`: ApiRefCommit,
)
object ApiRef {
def fromRef(
repositoryName: RepositoryName,
ref: Ref
): ApiRef =
ApiRef(
ref = ref.getName,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/${ref.getName}"),
`object` = ApiRefCommit(
sha = ref.getObjectId.getName,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${ref.getObjectId.getName}"),
`type` = "commit"
)
)
def fromTag(
repositoryName: RepositoryName,
tagInfo: TagInfo
): ApiRef =
ApiRef(
ref = s"refs/tags/${tagInfo.name}",
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/refs/tags/${tagInfo.name}"),
`object` = ApiRefCommit(
sha = tagInfo.objectId,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/tags/${tagInfo.objectId}"), // TODO This URL is not yet available?
`type` = "tag"
)
)
}

View File

@@ -1,29 +0,0 @@
package gitbucket.core.api
import gitbucket.core.util.RepositoryName
case class ApiTagCommit(
sha: String,
url: ApiPath
)
case class ApiTag(
name: String,
commit: ApiTagCommit,
zipball_url: ApiPath,
tarball_url: ApiPath
)
object ApiTag {
def apply(
tagName: String,
repositoryName: RepositoryName,
commitId: String
): ApiTag =
ApiTag(
name = tagName,
commit = ApiTagCommit(sha = commitId, url = ApiPath(s"/${repositoryName.fullName}/commits/${commitId}")),
zipball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.zip"),
tarball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.tar.gz")
)
}

View File

@@ -138,7 +138,7 @@ trait ReleaseControllerBase extends ControllerBase {
get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository => get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository =>
val Seq(previousTag, currentTag) = multiParams("splat") val Seq(previousTag, currentTag) = multiParams("splat")
val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.id }.getOrElse("") val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.commitId }.getOrElse("")
val commitLog = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => val commitLog = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val commits = JGitUtil.getCommitLog(git, previousTagId, currentTag).reverse val commits = JGitUtil.getCommitLog(git, previousTagId, currentTag).reverse

View File

@@ -1,7 +1,6 @@
package gitbucket.core.controller package gitbucket.core.controller
import java.io.FileInputStream import java.io.FileInputStream
import gitbucket.core.admin.html import gitbucket.core.admin.html
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService._ import gitbucket.core.service.SystemSettingsService._
@@ -50,8 +49,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
"ssh" -> mapping( "ssh" -> mapping(
"enabled" -> trim(label("SSH access", boolean())), "enabled" -> trim(label("SSH access", boolean())),
"host" -> trim(label("SSH host", optional(text()))), "bindAddress" -> mapping(
"port" -> trim(label("SSH port", optional(number()))) "host" -> trim(label("Bind SSH host", optional(text()))),
"port" -> trim(label("Bind SSH port", optional(number()))),
)(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
),
"publicAddress" -> mapping(
"host" -> trim(label("Public SSH host", optional(text()))),
"port" -> trim(label("Public SSH port", optional(number()))),
)(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
),
)(Ssh.apply), )(Ssh.apply),
"useSMTP" -> trim(label("SMTP", boolean())), "useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked( "smtp" -> optionalIfNotChecked(
@@ -116,8 +127,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
if (settings.ssh.enabled && settings.baseUrl.isEmpty) { if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
Some("baseUrl" -> "Base URL is required if SSH access is enabled.") Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else None, } else None,
if (settings.ssh.enabled && settings.ssh.sshHost.isEmpty) { if (settings.ssh.enabled && settings.ssh.bindAddress.isEmpty) {
Some("sshHost" -> "SSH host is required if SSH access is enabled.") Some("ssh.bindAddress.host" -> "SSH bind host is required if SSH access is enabled.")
} else None } else None
).flatten ).flatten
} }
@@ -308,12 +319,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form) saveSystemSettings(form)
if (form.sshAddress != context.settings.sshAddress) { if (form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress) {
SshServer.stop() SshServer.stop()
for { for {
sshAddress <- form.sshAddress bindAddress <- form.ssh.bindAddress
publicAddress <- form.ssh.publicAddress.orElse(form.ssh.bindAddress)
baseUrl <- form.baseUrl baseUrl <- form.baseUrl
} SshServer.start(sshAddress, baseUrl) } SshServer.start(bindAddress, publicAddress, baseUrl)
} }
flash.update("info", "System settings has been updated.") flash.update("info", "System settings has been updated.")

View File

@@ -1,9 +1,10 @@
package gitbucket.core.controller.api package gitbucket.core.controller.api
import gitbucket.core.api.{ApiObject, ApiRef, CreateARef, JsonFormat, UpdateARef} import gitbucket.core.api.{ApiError, ApiRef, CreateARef, JsonFormat, UpdateARef}
import gitbucket.core.controller.ControllerBase import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.Directory.getRepositoryDir import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.ReferrerAuthenticator
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.{ReferrerAuthenticator, RepositoryName, WritableUsersAuthenticator}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.RefUpdate.Result import org.eclipse.jgit.lib.RefUpdate.Result
@@ -14,47 +15,38 @@ import scala.jdk.CollectionConverters._
import scala.util.Using import scala.util.Using
trait ApiGitReferenceControllerBase extends ControllerBase { trait ApiGitReferenceControllerBase extends ControllerBase {
self: ReferrerAuthenticator => self: ReferrerAuthenticator with WritableUsersAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase]) private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase])
get("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
val result = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val refs = git
.getRepository()
.getRefDatabase()
.getRefsByPrefix("refs")
.asScala
refs.map(ApiRef.fromRef(RepositoryName(s"${repository.owner}/${repository.name}"), _))
}
JsonFormat(result)
})
/* /*
* i. Get a reference * i. Get a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#get-a-reference * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#get-a-reference
*/ */
get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository =>
getRef() val revstr = multiParams("splat").head
getRef(revstr, repository)
}) })
// Some versions of GHE support this path // Some versions of GHE support this path
get("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { repository =>
logger.warn("git/refs/ endpoint may not be compatible with GitHub API v3. Consider using git/ref/ endpoint instead") logger.warn("git/refs/ endpoint may not be compatible with GitHub API v3. Consider using git/ref/ endpoint instead")
getRef()
})
private def getRef() = {
val revstr = multiParams("splat").head val revstr = multiParams("splat").head
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => getRef(revstr, repository)
val ref = git.getRepository().findRef(revstr)
if (ref != null) {
val sha = ref.getObjectId().name()
JsonFormat(ApiRef(revstr, ApiObject(sha)))
} else {
val refs = git
.getRepository()
.getRefDatabase()
.getRefsByPrefix("refs/")
.asScala
JsonFormat(refs.map { ref =>
val sha = ref.getObjectId().name()
ApiRef(revstr, ApiObject(sha))
}) })
}
}
}
/* /*
* ii. Get all references * ii. Get all references
@@ -65,17 +57,17 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iii. Create a reference * iii. Create a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
*/ */
post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { _ => post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
extractFromJsonBody[CreateARef].map { extractFromJsonBody[CreateARef].map {
data => data =>
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
val ref = git.getRepository.findRef(data.ref) val ref = git.getRepository.findRef(data.ref)
if (ref == null) { if (ref == null) {
val update = git.getRepository.updateRef(data.ref) val update = git.getRepository.updateRef(data.ref)
update.setNewObjectId(ObjectId.fromString(data.sha)) update.setNewObjectId(ObjectId.fromString(data.sha))
val result = update.update() val result = update.update()
result match { result match {
case Result.NEW => JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName))) case Result.NEW => JsonFormat(ApiRef.fromRef(RepositoryName(repository.owner, repository.name), ref))
case _ => UnprocessableEntity(result.name()) case _ => UnprocessableEntity(result.name())
} }
} else { } else {
@@ -89,11 +81,11 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iv. Update a reference * iv. Update a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
*/ */
patch("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ => patch("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { repository =>
val refName = multiParams("splat").mkString("/") val refName = multiParams("splat").mkString("/")
extractFromJsonBody[UpdateARef].map { extractFromJsonBody[UpdateARef].map {
data => data =>
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
val ref = git.getRepository.findRef(refName) val ref = git.getRepository.findRef(refName)
if (ref == null) { if (ref == null) {
UnprocessableEntity("Ref does not exist.") UnprocessableEntity("Ref does not exist.")
@@ -104,7 +96,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
val result = update.update() val result = update.update()
result match { result match {
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE => case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName))) JsonFormat(ApiRef.fromRef(RepositoryName(repository), update.getRef))
case _ => UnprocessableEntity(result.name()) case _ => UnprocessableEntity(result.name())
} }
} }
@@ -116,7 +108,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* v. Delete a reference * v. Delete a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#delete-a-reference * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#delete-a-reference
*/ */
delete("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ => delete("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { _ =>
val refName = multiParams("splat").mkString("/") val refName = multiParams("splat").mkString("/")
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val ref = git.getRepository.findRef(refName) val ref = git.getRepository.findRef(refName)
@@ -133,4 +125,34 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
} }
} }
}) })
private def notFound(): ApiError = {
response.setStatus(404)
ApiError("Not Found")
}
protected def getRef(revstr: String, repository: RepositoryInfo): AnyRef = {
logger.debug(s"getRef: path '${revstr}'")
val name = RepositoryName(repository)
val result = JsonFormat(revstr match {
case "tags" => repository.tags.map(ApiRef.fromTag(name, _))
case x if x.startsWith("tags/") =>
val tagName = x.substring("tags/".length)
repository.tags.find(_.name == tagName) match {
case Some(tagInfo) => ApiRef.fromTag(name, tagInfo)
case None => notFound()
}
case other =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.getRepository().findRef(other) match {
case null => notFound()
case ref => ApiRef.fromRef(name, ref)
}
}
})
logger.debug(s"json result: $result")
result
}
} }

View File

@@ -2,7 +2,6 @@ package gitbucket.core.controller.api
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.controller.ControllerBase import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.MilestonesService import gitbucket.core.service.MilestonesService
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import org.scalatra.NoContent import org.scalatra.NoContent

View File

@@ -16,6 +16,7 @@ import scala.util.Using
trait ApiRepositoryControllerBase extends ControllerBase { trait ApiRepositoryControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService
with ApiGitReferenceControllerBase
with RepositoryCreationService with RepositoryCreationService
with AccountService with AccountService
with OwnerAuthenticator with OwnerAuthenticator
@@ -184,9 +185,11 @@ trait ApiRepositoryControllerBase extends ControllerBase {
* https://docs.github.com/en/rest/reference/repos#list-repository-tags * https://docs.github.com/en/rest/reference/repos#list-repository-tags
*/ */
get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
JsonFormat( JsonFormat(
repository.tags.map(tagInfo => ApiTag(tagInfo.name, RepositoryName(repository), tagInfo.id)) self.getRef("tags", repository)
) )
}
}) })
/* /*

View File

@@ -1,14 +1,15 @@
package gitbucket.core.plugin package gitbucket.core.plugin
import javax.servlet.ServletContext import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase} import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue} import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.service.SystemSettingsService.SystemSettings
import io.github.gitbucket.solidbase.model.Version import io.github.gitbucket.solidbase.model.Version
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.command.Command import org.apache.sshd.server.command.Command
import play.twirl.api.Html import play.twirl.api.Html
import scala.util.Using import scala.util.Using
/** /**
@@ -323,7 +324,7 @@ abstract class Plugin {
/** /**
* Override to add ssh command providers. * Override to add ssh command providers.
*/ */
val sshCommandProviders: Seq[PartialFunction[String, Command]] = Nil val sshCommandProviders: Seq[PartialFunction[String, ChannelSession => Command]] = Nil
/** /**
* Override to add ssh command providers. * Override to add ssh command providers.
@@ -332,7 +333,7 @@ abstract class Plugin {
registry: PluginRegistry, registry: PluginRegistry,
context: ServletContext, context: ServletContext,
settings: SystemSettings settings: SystemSettings
): Seq[PartialFunction[String, Command]] = Nil ): Seq[PartialFunction[String, ChannelSession => Command]] = Nil
/** /**
* This method is invoked in initialization of plugin system. * This method is invoked in initialization of plugin system.

View File

@@ -6,7 +6,6 @@ import java.nio.file.{Files, Paths, StandardWatchEventKinds}
import java.util.Base64 import java.util.Base64
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.servlet.ServletContext import javax.servlet.ServletContext
import com.github.zafarkhaja.semver.Version import com.github.zafarkhaja.semver.Version
import gitbucket.core.controller.{Context, ControllerBase} import gitbucket.core.controller.{Context, ControllerBase}
@@ -21,6 +20,7 @@ import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager 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.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.command.Command import org.apache.sshd.server.command.Command
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import play.twirl.api.Html import play.twirl.api.Html
@@ -58,7 +58,7 @@ class PluginRegistry {
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider] private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
suggestionProviders.add(new UserNameSuggestionProvider()) suggestionProviders.add(new UserNameSuggestionProvider())
suggestionProviders.add(new IssueSuggestionProvider()) suggestionProviders.add(new IssueSuggestionProvider())
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]() private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, ChannelSession => Command]]()
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo) def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
@@ -177,10 +177,11 @@ class PluginRegistry {
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq
def addSshCommandProvider(sshCommandProvider: PartialFunction[String, Command]): Unit = def addSshCommandProvider(sshCommandProvider: PartialFunction[String, ChannelSession => Command]): Unit =
sshCommandProviders.add(sshCommandProvider) sshCommandProviders.add(sshCommandProvider)
def getSshCommandProviders: Seq[PartialFunction[String, Command]] = sshCommandProviders.asScala.toSeq def getSshCommandProviders: Seq[PartialFunction[String, ChannelSession => Command]] =
sshCommandProviders.asScala.toSeq
} }
/** /**

View File

@@ -579,7 +579,7 @@ trait PullRequestService {
case (oldGit, newGit) => case (oldGit, newGit) =>
if (originRepository.branchList.contains(originId)) { if (originRepository.branchList.contains(originId)) {
val forkedId2 = val forkedId2 =
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId) forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.commitId }.getOrElse(forkedId)
val originId2 = JGitUtil.getForkedCommitId( val originId2 = JGitUtil.getForkedCommitId(
oldGit, oldGit,
@@ -596,9 +596,9 @@ trait PullRequestService {
} else { } else {
val originId2 = val originId2 =
originRepository.tags.collectFirst { case x if x.name == originId => x.id }.getOrElse(originId) originRepository.tags.collectFirst { case x if x.name == originId => x.commitId }.getOrElse(originId)
val forkedId2 = val forkedId2 =
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId) forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.commitId }.getOrElse(forkedId)
(Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2))) (Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2)))
} }

View File

@@ -12,6 +12,7 @@ import gitbucket.core.util.JGitUtil.FileInfo
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{Repository => _} import org.eclipse.jgit.lib.{Repository => _}
import scala.util.Using import scala.util.Using
trait RepositoryService { trait RepositoryService {
@@ -835,12 +836,10 @@ object RepositoryService {
def httpUrl(owner: String, name: String)(implicit context: Context): String = def httpUrl(owner: String, name: String)(implicit context: Context): String =
s"${context.baseUrl}/git/${owner}/${name}.git" s"${context.baseUrl}/git/${owner}/${name}.git"
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] = def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
if (context.settings.ssh.enabled) { context.settings.sshUrl(owner, name)
context.settings.sshAddress.map { x =>
s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git"
}
} else None
def openRepoUrl(openUrl: String)(implicit context: Context): String = def openRepoUrl(openUrl: String)(implicit context: Context): String =
s"github-${context.platform}://openRepo/${openUrl}" s"github-${context.platform}://openRepo/${openUrl}"

View File

@@ -4,9 +4,10 @@ import javax.servlet.http.HttpServletRequest
import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.oauth2.sdk.auth.Secret import com.nimbusds.oauth2.sdk.auth.Secret
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer} import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
import gitbucket.core.service.SystemSettingsService._ import gitbucket.core.service.SystemSettingsService.{getOptionValue, _}
import gitbucket.core.util.ConfigUtil._ import gitbucket.core.util.ConfigUtil._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import scala.util.Using import scala.util.Using
trait SystemSettingsService { trait SystemSettingsService {
@@ -29,8 +30,14 @@ trait SystemSettingsService {
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString) props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
props.setProperty(SshEnabled, settings.ssh.enabled.toString) props.setProperty(SshEnabled, settings.ssh.enabled.toString)
settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim)) settings.ssh.bindAddress.foreach { bindAddress =>
settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) props.setProperty(SshBindAddressHost, bindAddress.host.trim())
props.setProperty(SshBindAddressPort, bindAddress.port.toString)
}
settings.ssh.publicAddress.foreach { publicAddress =>
props.setProperty(SshPublicAddressHost, publicAddress.host.trim())
props.setProperty(SshPublicAddressPort, publicAddress.port.toString)
}
props.setProperty(UseSMTP, settings.useSMTP.toString) props.setProperty(UseSMTP, settings.useSMTP.toString)
if (settings.useSMTP) { if (settings.useSMTP) {
settings.smtp.foreach { smtp => settings.smtp.foreach { smtp =>
@@ -95,6 +102,10 @@ trait SystemSettingsService {
props.load(in) props.load(in)
} }
} }
loadSystemSettings(props)
}
def loadSystemSettings(props: java.util.Properties): SystemSettings = {
SystemSettings( SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue(props, Information, None), getOptionValue(props, Information, None),
@@ -112,9 +123,20 @@ trait SystemSettingsService {
getValue(props, Notification, false), getValue(props, Notification, false),
getValue(props, LimitVisibleRepositories, false), getValue(props, LimitVisibleRepositories, false),
Ssh( Ssh(
getValue(props, SshEnabled, false), enabled = getValue(props, SshEnabled, false),
getOptionValue[String](props, SshHost, None).map(_.trim), bindAddress = {
getOptionValue(props, SshPort, Some(DefaultSshPort)) // try the new-style configuration first
getOptionValue[String](props, SshBindAddressHost, None)
.map(h => SshAddress(h, getValue(props, SshBindAddressPort, DefaultSshPort), GenericSshUser))
.orElse(
// otherwise try to get old-style configuration
getOptionValue[String](props, SshHost, None)
.map(_.trim)
.map(h => SshAddress(h, getValue(props, SshPort, DefaultSshPort), GenericSshUser))
)
},
publicAddress = getOptionValue[String](props, SshPublicAddressHost, None)
.map(h => SshAddress(h, getValue(props, SshPublicAddressPort, PublicSshPort), GenericSshUser))
), ),
getValue( getValue(
props, props,
@@ -182,7 +204,6 @@ trait SystemSettingsService {
) )
) )
} }
} }
object SystemSettingsService { object SystemSettingsService {
@@ -214,7 +235,6 @@ object SystemSettingsService {
upload: Upload, upload: Upload,
repositoryViewer: RepositoryViewerSettings repositoryViewer: RepositoryViewerSettings
) { ) {
def baseUrl(request: HttpServletRequest): String = def baseUrl(request: HttpServletRequest): String =
baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/") baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
@@ -231,11 +251,17 @@ object SystemSettingsService {
.fold(base)(_ + base.dropWhile(_ != ':')) .fold(base)(_ + base.dropWhile(_ != ':'))
} }
def sshAddress: Option[SshAddress] = def sshBindAddress: Option[SshAddress] =
ssh.sshHost.collect { ssh.bindAddress
case host if ssh.enabled =>
SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git") def sshPublicAddress: Option[SshAddress] =
} ssh.publicAddress.orElse(ssh.bindAddress)
def sshUrl: Option[String] =
ssh.getUrl
def sshUrl(owner: String, name: String): Option[String] =
ssh.getUrl(owner: String, name: String)
} }
case class RepositoryOperation( case class RepositoryOperation(
@@ -248,9 +274,35 @@ object SystemSettingsService {
case class Ssh( case class Ssh(
enabled: Boolean, enabled: Boolean,
sshHost: Option[String], bindAddress: Option[SshAddress],
sshPort: Option[Int] publicAddress: Option[SshAddress]
) ) {
def getUrl: Option[String] =
if (enabled) {
publicAddress.map(_.getUrl).orElse(bindAddress.map(_.getUrl))
} else {
None
}
def getUrl(owner: String, name: String): Option[String] =
if (enabled) {
publicAddress
.map(_.getUrl(owner, name))
.orElse(bindAddress.map(_.getUrl(owner, name)))
} else {
None
}
}
object Ssh {
def apply(
enabled: Boolean,
bindAddress: Option[SshAddress],
publicAddress: Option[SshAddress]
): Ssh =
new Ssh(enabled, bindAddress, publicAddress.orElse(bindAddress))
}
case class Ldap( case class Ldap(
host: String, host: String,
@@ -296,7 +348,25 @@ object SystemSettingsService {
password: Option[String] password: Option[String]
) )
case class SshAddress(host: String, port: Int, genericUser: String) case class SshAddress(host: String, port: Int, genericUser: String) {
def isDefaultPort: Boolean =
port == PublicSshPort
def getUrl: String =
if (isDefaultPort) {
s"${genericUser}@${host}"
} else {
s"${genericUser}@${host}:${port}"
}
def getUrl(owner: String, name: String): String =
if (isDefaultPort) {
s"${genericUser}@${host}:${owner}/${name}.git"
} else {
s"ssh://${genericUser}@${host}:${port}/${owner}/${name}.git"
}
}
case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String]) case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
@@ -304,6 +374,8 @@ object SystemSettingsService {
case class RepositoryViewerSettings(maxFiles: Int) case class RepositoryViewerSettings(maxFiles: Int)
val GenericSshUser = "git"
val PublicSshPort = 22
val DefaultSshPort = 29418 val DefaultSshPort = 29418
val DefaultSmtpPort = 25 val DefaultSmtpPort = 25
val DefaultLdapPort = 389 val DefaultLdapPort = 389
@@ -325,6 +397,10 @@ object SystemSettingsService {
private val SshEnabled = "ssh" private val SshEnabled = "ssh"
private val SshHost = "ssh.host" private val SshHost = "ssh.host"
private val SshPort = "ssh.port" private val SshPort = "ssh.port"
private val SshBindAddressHost = "ssh.bindAddress.host"
private val SshBindAddressPort = "ssh.bindAddress.port"
private val SshPublicAddressHost = "ssh.publicAddress.host"
private val SshPublicAddressPort = "ssh.publicAddress.port"
private val UseSMTP = "useSMTP" private val UseSMTP = "useSMTP"
private val SmtpHost = "smtp.host" private val SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port" private val SmtpPort = "smtp.port"

View File

@@ -3,7 +3,6 @@ package gitbucket.core.servlet
import java.io.File import java.io.File
import java.util import java.util
import java.util.Date import java.util.Date
import scala.util.Using import scala.util.Using
import gitbucket.core.api import gitbucket.core.api
import gitbucket.core.api.JsonFormat.Context import gitbucket.core.api.JsonFormat.Context
@@ -209,9 +208,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
val settings = loadSystemSettings() val settings = loadSystemSettings()
val baseUrl = settings.baseUrl(request) val baseUrl = settings.baseUrl(request)
val sshUrl = settings.sshAddress.map { x => val sshUrl = settings.sshUrl(owner, repository)
s"${x.genericUser}@${x.host}:${x.port}"
}
if (!repository.endsWith(".wiki")) { if (!repository.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl) val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)

View File

@@ -5,26 +5,31 @@ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService} import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService}
import gitbucket.core.servlet.{CommitLogHook, Database} import gitbucket.core.servlet.{CommitLogHook, Database}
import gitbucket.core.util.Directory 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.command.{Command, CommandFactory}
import org.apache.sshd.server.session.ServerSession import org.apache.sshd.server.session.{ServerSession, ServerSessionAware}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.{File, InputStream, OutputStream}
import java.io.{File, InputStream, OutputStream}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import Directory._ import Directory._
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
import org.apache.sshd.server.channel.ChannelSession
import org.eclipse.jgit.transport.{ReceivePack, UploadPack} import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.shell.UnknownCommand import org.apache.sshd.server.shell.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException import org.eclipse.jgit.errors.RepositoryNotFoundException
import scala.util.Using import scala.util.Using
object GitCommand { object GitCommand {
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
val DefaultCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
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]) private val logger = LoggerFactory.getLogger(classOf[GitCommand])
@@ -57,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()) val thread = new Thread(newTask())
thread.start() thread.start()
} }
override def destroy(): Unit = {} override def destroy(channel: ChannelSession): Unit = {}
override def setExitCallback(callback: ExitCallback): Unit = { override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback this.callback = callback
@@ -159,7 +164,7 @@ class DefaultGitUploadPack(owner: String, repoName: String)
} }
} }
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String]) class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshAddress: SshAddress)
extends DefaultGitCommand(owner, repoName) extends DefaultGitCommand(owner, repoName)
with RepositoryService with RepositoryService
with AccountService with AccountService
@@ -177,7 +182,8 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, ss
val repository = git.getRepository val repository = git.getRepository
val receive = new ReceivePack(repository) val receive = new ReceivePack(repository)
if (!repoName.endsWith(".wiki")) { if (!repoName.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl) val hook =
new CommitLogHook(owner, repoName, userName(authType), baseUrl, Some(sshAddress.getUrl(owner, repoName)))
receive.setPreReceiveHook(hook) receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook) receive.setPostReceiveHook(hook)
} }
@@ -227,10 +233,10 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting)
} }
} }
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory { class GitCommandFactory(baseUrl: String, sshAddress: SshAddress) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory]) private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = { override def createCommand(channel: ChannelSession, command: String): Command = {
import GitCommand._ import GitCommand._
logger.debug(s"command: $command") logger.debug(s"command: $command")
@@ -238,17 +244,22 @@ class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends Command
case f if f.isDefinedAt(command) => f(command) case f if f.isDefinedAt(command) => f(command)
} }
pluginCommand match { pluginCommand.map(_.apply(channel)).getOrElse {
case Some(x) => x val (simpleRegex, defaultRegex) =
case None => if (sshAddress.isDefaultPort) {
(SimpleCommandRegexPort22, DefaultCommandRegexPort22)
} else {
(SimpleCommandRegex, DefaultCommandRegex)
}
command match { command match {
case SimpleCommandRegex("upload", repoName) if (pluginRepository(repoName)) => case simpleRegex("upload", repoName) if pluginRepository(repoName) =>
new PluginGitUploadPack(repoName, routing(repoName)) new PluginGitUploadPack(repoName, routing(repoName))
case SimpleCommandRegex("receive", repoName) if (pluginRepository(repoName)) => case simpleRegex("receive", repoName) if pluginRepository(repoName) =>
new PluginGitReceivePack(repoName, routing(repoName)) new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload", owner, repoName) => new DefaultGitUploadPack(owner, repoName) case defaultRegex("upload", owner, repoName) =>
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitUploadPack(owner, repoName)
new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl) case defaultRegex("receive", owner, repoName) =>
new DefaultGitReceivePack(owner, repoName, baseUrl, sshAddress)
case _ => new UnknownCommand(command) case _ => new UnknownCommand(command)
} }
} }

View File

@@ -1,20 +1,23 @@
package gitbucket.core.ssh package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService.SshAddress 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.{Environment, ExitCallback}
import org.apache.sshd.server.command.Command 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 import org.eclipse.jgit.lib.Constants
class NoShell(sshAddress: SshAddress) extends Factory[Command] { class NoShell(sshAddress: SshAddress) extends ShellFactory {
override def create(): Command = new Command() { override def createShell(channel: ChannelSession): Command = new Command() {
private var in: InputStream = null private var in: InputStream = null
private var out: OutputStream = null private var out: OutputStream = null
private var err: OutputStream = null private var err: OutputStream = null
private var callback: ExitCallback = 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 = val message =
""" """
| Welcome to | Welcome to
@@ -30,8 +33,8 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
| |
| Please use: | Please use:
| |
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git | git clone %s
""".stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n" """.stripMargin.format(placeholderAddress).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message)) err.write(Constants.encode(message))
err.flush() err.flush()
in.close() in.close()
@@ -40,7 +43,7 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
callback.onExit(127) callback.onExit(127)
} }
override def destroy(): Unit = {} override def destroy(channel: ChannelSession): Unit = {}
override def setInputStream(in: InputStream): Unit = { override def setInputStream(in: InputStream): Unit = {
this.in = in this.in = in

View File

@@ -8,13 +8,13 @@ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
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.AttributeStore import org.apache.sshd.common.AttributeRepository
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 authTypeSessionKey = new AttributeStore.AttributeKey[AuthType] private val authTypeSessionKey = new AttributeRepository.AttributeKey[AuthType]
def putAuthType(serverSession: ServerSession, authType: AuthType): Unit = def putAuthType(serverSession: ServerSession, authType: AuthType): Unit =
serverSession.setAttribute(authTypeSessionKey, authType) serverSession.setAttribute(authTypeSessionKey, authType)

View File

@@ -1,8 +1,7 @@
package gitbucket.core.ssh package gitbucket.core.ssh
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference
import javax.servlet.{ServletContextEvent, ServletContextListener} import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.Directory import gitbucket.core.util.Directory
@@ -11,40 +10,48 @@ import org.slf4j.LoggerFactory
object SshServer { object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass) private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer() private val server = new AtomicReference[org.apache.sshd.server.SshServer](null)
private val active = new AtomicBoolean(false)
private def configure(sshAddress: SshAddress, baseUrl: String) = { private def configure(
server.setPort(sshAddress.port) bindAddress: SshAddress,
publicAddress: SshAddress,
baseUrl: String
): org.apache.sshd.server.SshServer = {
val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
server.setPort(bindAddress.port)
val provider = new SimpleGeneratorHostKeyProvider( val provider = new SimpleGeneratorHostKeyProvider(
java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser") java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser")
) )
provider.setAlgorithm("RSA") provider.setAlgorithm("RSA")
provider.setOverwriteAllowed(false) provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider) server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser)) server.setPublickeyAuthenticator(new PublicKeyAuthenticator(bindAddress.genericUser))
server.setCommandFactory( server.setCommandFactory(
new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}")) new GitCommandFactory(baseUrl, publicAddress)
) )
server.setShellFactory(new NoShell(sshAddress)) server.setShellFactory(new NoShell(publicAddress))
server
} }
def start(sshAddress: SshAddress, baseUrl: String) = { def start(bindAddress: SshAddress, publicAddress: SshAddress, baseUrl: String): Unit = {
if (active.compareAndSet(false, true)) { this.server.synchronized {
configure(sshAddress, baseUrl) val server = configure(bindAddress, publicAddress, baseUrl)
if (this.server.compareAndSet(null, server)) {
server.start() server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}") logger.info(s"Start SSH Server Listen on ${server.getPort}")
} }
} }
}
def stop() = { def stop(): Unit = {
if (active.compareAndSet(true, false)) { this.server.synchronized {
server.stop(true) val server = this.server.getAndSet(null)
if (server != null) {
server.stop()
logger.info("SSH Server is stopped.") logger.info("SSH Server is stopped.")
} }
} }
}
def isActive = active.get
} }
/* /*
@@ -59,13 +66,14 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
override def contextInitialized(sce: ServletContextEvent): Unit = { override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings() val settings = loadSystemSettings()
if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) { if (settings.sshBindAddress.isDefined && settings.baseUrl.isEmpty) {
logger.error("Could not start SshServer because the baseUrl is not configured.") logger.error("Could not start SshServer because the baseUrl is not configured.")
} }
for { for {
sshAddress <- settings.sshAddress bindAddress <- settings.sshBindAddress
publicAddress <- settings.sshPublicAddress
baseUrl <- settings.baseUrl baseUrl <- settings.baseUrl
} SshServer.start(sshAddress, baseUrl) } SshServer.start(bindAddress, publicAddress, baseUrl)
} }
override def contextDestroyed(sce: ServletContextEvent): Unit = { override def contextDestroyed(sce: ServletContextEvent): Unit = {

View File

@@ -22,9 +22,7 @@ object Implicits {
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request) implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x => JsonFormat.Context(context.baseUrl, context.settings.sshUrl)
s"${x.genericUser}@${x.host}:${x.port}"
})
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal { implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {

View File

@@ -228,10 +228,11 @@ object JGitUtil {
* *
* @param name the tag name * @param name the tag name
* @param time the tagged date * @param time the tagged date
* @param id the commit id * @param commitId the commit id
* @param message the message of the tagged commit * @param message the message of the tagged commit
* @param objectId the tag object id
*/ */
case class TagInfo(name: String, time: Date, id: String, message: String) case class TagInfo(name: String, time: Date, commitId: String, message: String, objectId: String)
/** /**
* The submodule data * The submodule data
@@ -347,7 +348,8 @@ object JGitUtil {
ref.getName.stripPrefix("refs/tags/"), ref.getName.stripPrefix("refs/tags/"),
revCommit.getCommitterIdent.getWhen, revCommit.getCommitterIdent.getWhen,
revCommit.getName, revCommit.getName,
revCommit.getShortMessage revCommit.getShortMessage,
ref.getObjectId.getName
) )
) )
} catch { } catch {

View File

@@ -19,22 +19,40 @@
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/> <input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
Enable SSH access to git repository Enable SSH access to git repository
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span> <span class="muted normal">(Both SSH bind host and Base URL are required if SSH access is enabled)</span>
</label> </label>
</fieldset> </fieldset>
<div class="ssh"> <div class="ssh">
<div class="bindAddress">
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="sshHost">SSH host</label> <label class="control-label col-md-2" for="sshBindHost">SSH bind host</label>
<div class="col-md-10"> <div class="col-md-10">
<input type="text" id="sshHost" name="ssh.host" class="form-control" value="@context.settings.ssh.sshHost"/> <input type="text" id="sshBindHost" name="ssh.bindAddress.host" class="form-control" value="@context.settings.ssh.bindAddress.map(_.host)"/>
<span id="error-ssh_host" class="error"></span> <span id="error-ssh_bindAddress_host" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="sshPort">SSH port</label> <label class="control-label col-md-2" for="sshBindPort">SSH bind port</label>
<div class="col-md-10"> <div class="col-md-10">
<input type="text" id="sshPort" name="ssh.port" class="form-control" value="@context.settings.ssh.sshPort"/> <input type="text" id="sshBindPort" name="ssh.bindAddress.port" class="form-control" value="@context.settings.ssh.bindAddress.map(_.port)"/>
<span id="error-ssh_port" class="error"></span> <span id="error-ssh_bindAddress_port" class="error"></span>
</div>
</div>
</div>
<div class="publicAddress">
<div class="form-group">
<label class="control-label col-md-2" for="sshPublicHost">SSH public host</label>
<div class="col-md-10">
<input type="text" id="sshPublicHost" name="ssh.publicAddress.host" class="form-control" value="@context.settings.ssh.publicAddress.map(_.host)"/>
<span id="error-ssh_publicAddress_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshPublicPort">SSH public port</label>
<div class="col-md-10">
<input type="text" id="sshPublicPort" name="ssh.publicAddress.port" class="form-control" value="@context.settings.ssh.publicAddress.map(_.port)"/>
<span id="error-ssh_publicAddress_port" class="error"></span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -40,8 +40,6 @@
</div> </div>
</div> </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> <script>
$(function(){ $(function(){
@if(elastic){ @if(elastic){

View File

@@ -16,7 +16,7 @@
<td> <td>
<div class="col-md-2 text-right"> <div class="col-md-2 text-right">
<a href="@helpers.url(repository)/tree/@helpers.urlEncode(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br> <a href="@helpers.url(repository)/tree/@helpers.urlEncode(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br>
<a href="@helpers.url(repository)/commit/@tag.id" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.id.substring(0, 7)</a><br> <a href="@helpers.url(repository)/commit/@tag.commitId" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.commitId.substring(0, 7)</a><br>
<span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span> <span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span>
</div> </div>
<div class="col-md-10" style="border-left: 1px solid #eee"> <div class="col-md-10" style="border-left: 1px solid #eee">

View File

@@ -10,7 +10,7 @@
@defining(repository.tags.find(_.name == release.tag)){ tag => @defining(repository.tags.find(_.name == release.tag)){ tag =>
@tag.map { tag => @tag.map { tag =>
<a href="@helpers.url(repository)/tree/@helpers.urlEncode(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br> <a href="@helpers.url(repository)/tree/@helpers.urlEncode(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br>
<a href="@helpers.url(repository)/commit/@tag.id" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.id.substring(0, 7)</a><br> <a href="@helpers.url(repository)/commit/@tag.commitId" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.commitId.substring(0, 7)</a><br>
<span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span> <span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span>
} }
} }

View File

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

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>gitbucket.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
-->
<logger name="gitbucket" level="DEBUG"/>
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
<!--
<logger name="service.WebHookService" level="DEBUG" />
<logger name="servlet" level="DEBUG" />
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
-->
</configuration>

View File

@@ -52,6 +52,8 @@ class TestingGitBucketServer(val port: Int = 19999) extends AutoCloseable {
def client(login: String, password: String): GitHub = def client(login: String, password: String): GitHub =
GitHub.connectToEnterprise(s"http://localhost:${port}/api/v3", login, password) GitHub.connectToEnterprise(s"http://localhost:${port}/api/v3", login, password)
def getDirectory(): File = dir
private def addStatisticsHandler(handler: Handler) = { // The graceful shutdown is implemented via the statistics handler. private def addStatisticsHandler(handler: Handler) = { // The graceful shutdown is implemented via the statistics handler.
// See the following: https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142 // See the following: https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142
val statisticsHandler = new StatisticsHandler val statisticsHandler = new StatisticsHandler

View File

@@ -2,10 +2,13 @@ package gitbucket.core.api
import gitbucket.core.TestingGitBucketServer import gitbucket.core.TestingGitBucketServer
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.eclipse.jgit.api.Git
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import scala.util.Using import scala.util.Using
import org.kohsuke.github.{GHCommitState, GitHub} import org.kohsuke.github.GHCommitState
import java.io.File
/** /**
* Need to run `sbt package` before running this test. * Need to run `sbt package` before running this test.
@@ -134,6 +137,27 @@ class ApiIntegrationTest extends AnyFunSuite {
assert(statusList.get(1).getState == GHCommitState.FAILURE) assert(statusList.get(1).getState == GHCommitState.FAILURE)
assert(statusList.get(1).getContext == "context") assert(statusList.get(1).getContext == "context")
} }
// get master ref
{
val ref = repo.getRef("heads/master")
assert(ref.getRef == "refs/heads/master")
assert(
ref.getUrl.toString == "http://localhost:19999/api/v3/repos/root/create_status_test/git/refs/heads/master"
)
assert(ref.getObject.getType == "commit")
}
// get tag v1.0
{
Using.resource(Git.open(new File(server.getDirectory(), "repositories/root/create_status_test"))) { git =>
git.tag().setName("v1.0").call()
}
val ref = repo.getRef("tags/v1.0")
assert(ref.getRef == "refs/tags/v1.0")
assert(ref.getUrl.toString == "http://localhost:19999/api/v3/repos/root/create_status_test/git/refs/tags/v1.0")
assert(ref.getObject.getType == "tag")
}
} }
} }

View File

@@ -83,8 +83,20 @@ object ApiSpecModels {
milestoneCount = 1, milestoneCount = 1,
branchList = Seq("master", "develop"), branchList = Seq("master", "develop"),
tags = Seq( tags = Seq(
TagInfo(name = "v1.0", time = date("2015-05-05T23:40:27Z"), id = "id1", message = "1.0 released"), TagInfo(
TagInfo(name = "v2.0", time = date("2016-05-05T23:40:27Z"), id = "id2", message = "2.0 released") name = "v1.0",
time = date("2015-05-05T23:40:27Z"),
commitId = "id1",
message = "1.0 released",
objectId = "id1"
),
TagInfo(
name = "v2.0",
time = date("2016-05-05T23:40:27Z"),
commitId = "id2",
message = "2.0 released",
objectId = "id2"
)
), ),
managers = Seq("myboss") managers = Seq("myboss")
) )
@@ -432,9 +444,29 @@ object ApiSpecModels {
val apiPusher = ApiPusher(account) val apiPusher = ApiPusher(account)
val apiRef = ApiRef( //have both urls as https, as the expected samples are using https
ref = "refs/heads/featureA", val gitHubContext = JsonFormat.Context("https://api.github.com", Some("https://api.github.com"))
`object` = ApiObject(sha1)
val apiRefHeadsMaster = ApiRef(
ref = "refs/heads/master",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/heads/master"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
`object` = ApiRefCommit(
sha = "6b2d124d092402f2c2b7131caada05ead9e7de6d",
`type` = "commit",
url = ApiPath("/repos/gitbucket/gitbucket/git/commits/6b2d124d092402f2c2b7131caada05ead9e7de6d")
)
)
val apiRefTag = ApiRef(
ref = "refs/tags/1.0",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/tags/1.0"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
`object` = ApiRefCommit(
sha = "1f164ecf2f59190afc8d7204a221c739e707df4c",
`type` = "tag",
url = ApiPath("/repos/gitbucket/gitbucket/git/tags/1f164ecf2f59190afc8d7204a221c739e707df4c")
)
) )
val assetFileName = "010203040a0b0c0d" val assetFileName = "010203040a0b0c0d"
@@ -765,8 +797,33 @@ object ApiSpecModels {
val jsonPusher = """{"name":"octocat","email":"octocat@example.com"}""" val jsonPusher = """{"name":"octocat","email":"octocat@example.com"}"""
//I checked all refs in gitbucket repo, and there appears to be only type "commit" and type "tag"
val jsonRef = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}""" val jsonRef = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonRefHeadsMaster =
"""{
|"ref": "refs/heads/master",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
|"url": "https://api.github.com/repos/gitbucket/gitbucket/git/refs/heads/master",
|"object": {
|"sha": "6b2d124d092402f2c2b7131caada05ead9e7de6d",
|"type": "commit",
|"url": "https://api.github.com/repos/gitbucket/gitbucket/git/commits/6b2d124d092402f2c2b7131caada05ead9e7de6d"
|}
|}""".stripMargin
val jsonRefTag =
"""{
|"ref": "refs/tags/1.0",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
|"url": "https://api.github.com/repos/gitbucket/gitbucket/git/refs/tags/1.0",
|"object": {
|"sha": "1f164ecf2f59190afc8d7204a221c739e707df4c",
|"type": "tag",
|"url": "https://api.github.com/repos/gitbucket/gitbucket/git/tags/1f164ecf2f59190afc8d7204a221c739e707df4c"
|}
|}""".stripMargin
val jsonReleaseAsset = val jsonReleaseAsset =
s"""{ s"""{
|"name":"release.zip", |"name":"release.zip",

View File

@@ -8,6 +8,12 @@ class JsonFormatSpec extends AnyFunSuite {
implicit val format = JsonFormat.jsonFormats implicit val format = JsonFormat.jsonFormats
private def expected(json: String) = json.replaceAll("\n", "") private def expected(json: String) = json.replaceAll("\n", "")
def normalizeJson(json: String) = {
org.json4s.jackson.parseJson(json)
}
def assertEqualJson(actual: String, expected: String) = {
assert(normalizeJson(actual) == normalizeJson(expected))
}
test("apiUser") { test("apiUser") {
assert(JsonFormat(apiUser) == expected(jsonUser)) assert(JsonFormat(apiUser) == expected(jsonUser))
@@ -76,8 +82,11 @@ class JsonFormatSpec extends AnyFunSuite {
test("apiPusher") { test("apiPusher") {
assert(JsonFormat(apiPusher) == expected(jsonPusher)) assert(JsonFormat(apiPusher) == expected(jsonPusher))
} }
test("apiRef") { test("apiRefHead") {
assert(JsonFormat(apiRef) == expected(jsonRef)) assertEqualJson(JsonFormat(apiRefHeadsMaster)(gitHubContext), jsonRefHeadsMaster)
}
test("apiRefTag") {
assertEqualJson(JsonFormat(apiRefTag)(gitHubContext), jsonRefTag)
} }
test("apiReleaseAsset") { test("apiReleaseAsset") {
assert(JsonFormat(apiReleaseAsset) == expected(jsonReleaseAsset)) assert(JsonFormat(apiReleaseAsset) == expected(jsonReleaseAsset))

View File

@@ -47,8 +47,8 @@ trait ServiceSpecBase {
limitVisibleRepositories = false, limitVisibleRepositories = false,
ssh = Ssh( ssh = Ssh(
enabled = false, enabled = false,
sshHost = None, bindAddress = None,
sshPort = None publicAddress = None
), ),
useSMTP = false, useSMTP = false,
smtp = None, smtp = None,

View File

@@ -0,0 +1,113 @@
package gitbucket.core.service
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.Properties
class SystemSettingsServiceSpec extends AnyWordSpecLike with Matchers {
"loadSystemSettings" should {
"read old-style ssh configuration" in new SystemSettingsService {
val props = new Properties()
props.setProperty("ssh", "true")
props.setProperty("ssh.host", "127.0.0.1")
props.setProperty("ssh.port", "8022")
val settings = loadSystemSettings(props)
settings.ssh.enabled shouldBe true
settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 8022, "git"))
settings.ssh.publicAddress shouldBe settings.ssh.bindAddress
}
"read new-style ssh configuration" in new SystemSettingsService {
val props = new Properties()
props.setProperty("ssh", "true")
props.setProperty("ssh.bindAddress.host", "127.0.0.1")
props.setProperty("ssh.bindAddress.port", "8022")
props.setProperty("ssh.publicAddress.host", "code.these.solutions")
props.setProperty("ssh.publicAddress.port", "22")
val settings = loadSystemSettings(props)
settings.ssh.enabled shouldBe true
settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 8022, "git"))
settings.ssh.publicAddress shouldBe Some(SshAddress("code.these.solutions", 22, "git"))
}
"default the ssh port if not specified" in new SystemSettingsService {
val props = new Properties()
props.setProperty("ssh", "true")
props.setProperty("ssh.bindAddress.host", "127.0.0.1")
props.setProperty("ssh.publicAddress.host", "code.these.solutions")
val settings = loadSystemSettings(props)
settings.ssh.enabled shouldBe true
settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 29418, "git"))
settings.ssh.publicAddress shouldBe Some(SshAddress("code.these.solutions", 22, "git"))
}
"default the public address if not specified" in new SystemSettingsService {
val props = new Properties()
props.setProperty("ssh", "true")
props.setProperty("ssh.bindAddress.host", "127.0.0.1")
props.setProperty("ssh.bindAddress.port", "8022")
val settings = loadSystemSettings(props)
settings.ssh.enabled shouldBe true
settings.ssh.bindAddress shouldBe Some(SshAddress("127.0.0.1", 8022, "git"))
settings.ssh.publicAddress shouldBe settings.ssh.bindAddress
}
"return addresses even if ssh is not enabled" in new SystemSettingsService {
val props = new Properties()
props.setProperty("ssh", "false")
props.setProperty("ssh.bindAddress.host", "127.0.0.1")
props.setProperty("ssh.bindAddress.port", "8022")
props.setProperty("ssh.publicAddress.host", "code.these.solutions")
props.setProperty("ssh.publicAddress.port", "22")
val settings = loadSystemSettings(props)
settings.ssh.enabled shouldBe false
settings.ssh.bindAddress shouldNot be(empty)
settings.ssh.publicAddress shouldNot be(empty)
}
}
"SshAddress" can {
trait MockContext {
val host = "code.these.solutions"
val port = 1337
val user = "git"
lazy val sshAddress = SshAddress(host, port, user)
}
"isDefaultPort" which {
"returns true if using port 22" in new MockContext {
override val port = 22
sshAddress.isDefaultPort shouldBe true
}
"returns false if using a different port" in new MockContext {
override val port = 8022
sshAddress.isDefaultPort shouldBe false
}
}
"getUrl" which {
"returns the port number when not using port 22" in new MockContext {
override val port = 8022
sshAddress.getUrl shouldBe "git@code.these.solutions:8022"
}
"leaves off the port number when using port 22" in new MockContext {
override val port = 22
sshAddress.getUrl shouldBe "git@code.these.solutions"
}
}
"getUrl for owner and repo" which {
"returns an ssh-protocol url when not using port 22" in new MockContext {
override val port = 8022
sshAddress.getUrl("np-hard", "quantum-crypto-cracker") shouldBe
"ssh://git@code.these.solutions:8022/np-hard/quantum-crypto-cracker.git"
}
"returns a bare-protocol url when using port 22" in new MockContext {
override val port = 22
sshAddress.getUrl("syntactic", "brace-stretcher") shouldBe
"git@code.these.solutions:syntactic/brace-stretcher.git"
}
}
}
}

View File

@@ -1,38 +1,102 @@
package gitbucket.core.ssh 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.apache.sshd.server.shell.UnknownCommand
import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class GitCommandFactorySpec extends AnyFunSpec { class GitCommandFactorySpec extends AnyWordSpec with Matchers {
val factory = new GitCommandFactory("http://localhost:8080", None) trait MockContext {
val baseUrl = "https://some.example.tech:8080/code-context"
val sshHost = "localhost"
val sshPort = 2222
lazy val factory = new GitCommandFactory(baseUrl, SshAddress(sshHost, sshPort, "git"))
}
describe("createCommand") { "createCommand" when {
it("should return GitReceivePack when command is git-receive-pack") { val channel = new ChannelSession()
assert(factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack] == true) "receiving a git-receive-pack command" should {
"return DefaultGitReceivePack" when {
"the path matches owner/repo" in new MockContext {
assert( assert(
factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack] == true factory.createCommand(channel, "git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack]
)
assert(
factory
.createCommand(channel, "git-receive-pack '/owner/repo.wiki.git'")
.isInstanceOf[DefaultGitReceivePack]
) )
} }
it("should return GitUploadPack when command is git-upload-pack") { "the leading slash is left off and running on port 22" in new MockContext {
assert(factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[DefaultGitUploadPack] == true) override val sshPort: Int = 22
assert(factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack] == true) assert(
} factory.createCommand(channel, "git-receive-pack 'owner/repo.git'").isInstanceOf[DefaultGitReceivePack]
it("should return UnknownCommand when command is not git-(upload|receive)-pack") { )
assert(factory.createCommand("git- '/owner/repo.git'").isInstanceOf[UnknownCommand] == true) assert(
assert(factory.createCommand("git-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true) factory.createCommand(channel, "git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack]
assert(factory.createCommand("git-a-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true) )
assert(factory.createCommand("git-up-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true) }
assert(factory.createCommand("\ngit-upload-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true) }
} "return UnknownCommand" when {
it("should return UnknownCommand when git command has no valid arguments") { "the ssh port is not 22 and the leading slash is missing" in new MockContext {
// must be: git-upload-pack '/owner/repository_name.git' override val sshPort: Int = 1337
assert(factory.createCommand("git-upload-pack").isInstanceOf[UnknownCommand] == true) assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack /owner/repo.git").isInstanceOf[UnknownCommand] == true) assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand] == true) assert(factory.createCommand(channel, "git-receive-pack 'oranges.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack '/ownerrepo.git'").isInstanceOf[UnknownCommand] == true) assert(factory.createCommand(channel, "git-receive-pack 'apples.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand("git-upload-pack '/owner/repo.wiki'").isInstanceOf[UnknownCommand] == true) }
"the path is malformed" in new MockContext {
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(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(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(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(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(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])
}
} }
} }
} }

View File

@@ -166,8 +166,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
limitVisibleRepositories = false, limitVisibleRepositories = false,
ssh = Ssh( ssh = Ssh(
enabled = false, enabled = false,
sshHost = None, bindAddress = None,
sshPort = None publicAddress = None
), ),
useSMTP = false, useSMTP = false,
smtp = None, smtp = None,

View File

@@ -1,7 +1,6 @@
package gitbucket.core.view package gitbucket.core.view
import gitbucket.core.util.SyntaxSugars import gitbucket.core.util.SyntaxSugars
import SyntaxSugars._
import org.scalatest.funspec.AnyFunSpec import org.scalatest.funspec.AnyFunSpec
class PaginationSpec extends AnyFunSpec { class PaginationSpec extends AnyFunSpec {