Compare commits

...

105 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
Naoki Takezoe
7f665c649b Fix duplicated diff issue in exporting patch feature(#2913) 2021-11-05 19:31:52 +09:00
Scala Steward
dcbadb4071 Update sbt-scoverage to 1.9.2 2021-11-03 02:44:03 +09:00
Mira
e3096d15ff resolve #2907 API - not milestone data in issue list (#2908) 2021-11-03 01:23:18 +09:00
Scala Steward
a83c24e7b3 Update scala-library to 2.13.7 2021-11-02 07:15:21 +09:00
Scala Steward
73457c9658 Update testcontainers-scala to 0.39.11 2021-11-02 06:59:06 +09:00
Naoki Takezoe
cfc8d9f3f1 Relax max password length limitation to 40 (#2906) 2021-10-31 11:30:39 +09:00
Scala Steward
8f423b83ea Update postgresql to 42.3.1 2021-10-30 07:15:00 +09:00
Scala Steward
1e7ac532b6 Update testcontainers-scala to 0.39.10 2021-10-28 12:17:14 +09:00
Scala Steward
0fd1db4596 Update mysql, postgresql to 1.16.2 2021-10-23 06:48:28 +09:00
Scala Steward
0755b7ab7f Update testcontainers-scala to 0.39.9 2021-10-22 07:58:09 +09:00
Scala Steward
052382e5c4 Update github-api to 1.135 2021-10-22 07:22:16 +09:00
Scala Steward
f44a63cec1 Update oauth2-oidc-sdk to 9.19 2021-10-20 22:36:12 +09:00
Scala Steward
603d67354a Update mysql, postgresql to 1.16.1 2021-10-19 22:06:56 +09:00
Scala Steward
aafa423b9f Update postgresql to 42.3.0 2021-10-19 06:59:29 +09:00
Mészáros István
2da9d0a801 Add HTTPS support for JettyLauncher (#2896) 2021-10-14 11:27:26 +09:00
Scala Steward
b60c112a74 Update sbt-scoverage to 1.9.1 2021-10-11 19:04:31 +09:00
Scala Steward
843ed6df37 Update mockito-core to 4.0.0 2021-10-07 23:48:03 +09:00
Scala Steward
0f0a849677 Update jetty-continuation, jetty-http, ... to 9.4.44.v20210927 2021-10-01 12:15:06 +09:00
Scala Steward
682901ccbb Update oauth2-oidc-sdk to 9.18 2021-09-30 17:50:08 +09:00
Naoki Takezoe
048fdb8837 Remove unnecessary workaround for scala-xml version conflict (#2888) 2021-09-27 03:00:15 +09:00
scala-steward-bot
3353616789 Update scalatra, scalatra-forms, ... to 2.8.2 (#2886) 2021-09-25 19:07:58 +09:00
scala-steward-bot
b6cb4c865f Update scalatra, scalatra-forms, ... to 2.8.1 (#2885) 2021-09-25 16:00:15 +09:00
kenji yoshida
1fcfd093f7 add jdk 17 test 2021-09-25 10:51:36 +09:00
Scala Steward
3f27c6e733 Update postgresql to 42.2.24 2021-09-23 21:00:09 +09:00
Scala Steward
b6bd9bfc3b Update oauth2-oidc-sdk to 9.17 2021-09-22 07:27:44 +09:00
Scala Steward
6c392f0056 Update oauth2-oidc-sdk to 9.16 2021-09-21 21:18:30 +09:00
Scala Steward
9a38de9a23 Update testcontainers-scala to 0.39.8 2021-09-20 19:57:18 +09:00
Scala Steward
8883600090 Update sbt-scoverage to 1.9.0 2021-09-17 01:27:52 +09:00
Naoki Takezoe
ab822a3c27 Fix Wiki page editing bug when over 100 pages (#2869) 2021-09-13 00:48:38 +09:00
Scala Steward
0e4d64de23 Update org.eclipse.jgit.archive, ... to 5.13.0.202109080827-r 2021-09-12 23:08:48 +09:00
Scala Steward
fbc6bd36bd Update logback-classic to 1.2.6 2021-09-10 13:09:30 +09:00
Naoki Takezoe
ed90ca2dce Bump gitbucket-pages-plugin to 1.10.0 (#2856) 2021-09-10 09:03:51 +09:00
Scala Steward
537ef92149 Update github-api to 1.133 2021-09-09 15:56:16 +09:00
Scala Steward
d51afa7d40 Update java-diff-utils to 4.11 2021-09-08 21:27:57 +09:00
Scala Steward
975cffff48 Update sbt-assembly to 1.1.0 2021-09-01 23:08:42 +09:00
Scala Steward
d92e9c00e8 Update testcontainers-scala to 0.39.7 2021-08-31 14:52:37 +09:00
KN4CK3R
12d72cbb19 Add support for "all" in issue list API (#2859) 2021-08-31 00:38:38 +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
Scala Steward
d8e03bed1f Update mockito-core to 3.12.4 2021-08-26 08:43:19 +09:00
Scala Steward
f48c087cd8 Update mockito-core to 3.12.3 2021-08-25 08:39:46 +09:00
Scala Steward
917663e0df Update mockito-core to 3.12.2 2021-08-25 06:11:05 +09:00
Scala Steward
556ddbc926 Update tika-core to 2.1.0 2021-08-24 23:07:23 +09:00
Scala Steward
1c6f37b8e8 Update testcontainers-scala to 0.39.6 2021-08-23 15:26:54 +09:00
Scala Steward
720a329a50 Update oauth2-oidc-sdk to 9.15 2021-08-21 07:09:24 +09:00
Scala Steward
220a8f076a Update mockito-core to 3.12.1 2021-08-21 07:09:08 +09:00
Scala Steward
43be8333c7 Update oauth2-oidc-sdk to 9.14 2021-08-20 21:08:36 +09:00
Scala Steward
08706ab4df Update mockito-core to 3.12.0 2021-08-20 08:04:50 +09:00
Naoki Takezoe
b1196657e0 Release GitBucket 4.36.2 2021-08-16 01:11:21 +09:00
Naoki Takezoe
334bd0c919 Escape user name in avatar image tag (#2845) 2021-08-16 01:06:16 +09:00
Scala Steward
cf0f896972 Update mariadb-java-client to 2.7.4 2021-08-12 07:10:24 +09:00
Scala Steward
d21ca3ff8a Update oauth2-oidc-sdk to 9.12 2021-08-11 19:08:07 +09:00
xuwei-k
83f1f16de7 remove == true 2021-08-09 16:56:55 +09:00
xuwei-k
0fa2ccf107 use keys 2021-08-09 16:48:03 +09:00
xuwei-k
18e3dd431b use until instead of to 2021-08-09 16:48:03 +09:00
xuwei-k
f25dee2249 .getOrElse(None) => .flatten 2021-08-09 16:48:03 +09:00
kenji yoshida
575ffa9580 avoid Array.toString 2021-08-09 16:43:09 +09:00
xuwei-k
f17af5aeb0 use forall or exists instead of map(f).getOrElse 2021-08-09 16:40:37 +09:00
xuwei-k
639f153589 use sizeIs 2021-08-09 16:38:16 +09:00
xuwei-k
fb07098c13 map(f).flatten => flatMap 2021-08-09 16:30:40 +09:00
xuwei-k
74fc08b039 use foreach instead of map 2021-08-09 16:28:15 +09:00
xuwei-k
2776e00004 fix typo 2021-08-09 16:25:55 +09:00
xuwei-k
5932fce303 remove semicolons 2021-08-09 16:12:41 +09:00
xuwei-k
39c9fc4261 remove redundant toString 2021-08-09 16:10:23 +09:00
kenji yoshida
89ea4509a3 use SAM conversion 2021-08-09 16:08:07 +09:00
kenji yoshida
d19d838ead getOrElse(null) => orNull 2021-08-09 16:05:22 +09:00
kenji yoshida
633a2699a8 find(f).isDefined => exists(f) 2021-08-09 16:03:22 +09:00
kenji yoshida
e79bca4a3c remove unnecessary local variable initial values 2021-08-09 16:01:02 +09:00
kenji yoshida
f8ab480d20 remove unused import 2021-08-09 15:55:52 +09:00
kenji yoshida
a14129e340 use diamond operator 2021-08-09 15:55:03 +09:00
Scala Steward
5ec39df6e0 Update oauth2-oidc-sdk to 9.11 2021-08-02 07:07:47 +09:00
kenji yoshida
956e0c6321 fix warning in build.sbt
02a0cfa0a6/io/src/main/scala/sbt/io/IO.scala (L611-L630)

```
/home/runner/work/gitbucket/gitbucket/build.sbt:205: warning: method jar in object IO is deprecated (since 1.3.2): Please specify whether to use a static timestamp
  IO jar (contentMappings.map { case (file, path) => (file, path.toString) }, outputFile, manifest)
     ^
```
2021-07-31 16:14:15 +09:00
Scala Steward
0660a9203a Update oauth2-oidc-sdk to 9.10.2 2021-07-27 23:30:19 +09:00
kenji yoshida
1b660272a1 prepare Scala 3 2021-07-27 07:23:01 +09:00
Scala Steward
d9ef9b874d Update logback-classic to 1.2.5 2021-07-27 06:56:21 +09:00
Scala Steward
7824f796ee Update json4s-jackson to 4.0.3 2021-07-27 03:01:31 +09:00
Scala Steward
838a8d4c7b Update scalatra, scalatra-forms, ... to 2.8.0 (#2834)
Co-authored-by: kenji yoshida <6b656e6a69@gmail.com>
2021-07-25 09:30:05 +09:00
Scala Steward
8db9f77f91 Update github-api to 1.132 2021-07-23 10:22:45 +09:00
76 changed files with 1154 additions and 505 deletions

View File

@@ -8,11 +8,11 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
java: [8, 11]
java: [8, 11, 17]
steps:
- uses: actions/checkout@v2
- name: Cache
uses: actions/cache@v2.1.6
uses: actions/cache@v2.1.7
env:
cache-name: cache-sbt-libs
with:

View File

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

View File

@@ -1,6 +1,22 @@
# Changelog
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
- Escape user name in avatar image tag
### 4.36.1 - 22 Jul 2021
- Bump gitbucket-gist-plugin to 4.21.0

View File

@@ -61,15 +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.
- 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.1 - 22 Jul 2021
- Bump gitbucket-gist-plugin to 4.21.0
### 4.37.1 - 14 Dec 2021
- Update gist-plugin and notification-plugin
- Fix SSHCommand extension point for apache-sshd 2.x
### 4.36.0 - 17 Jul 2021
- Tag selector in the repository viewer
- Link issues/pull requests of other repositories
- Files and lines can be linked in the diff view
- Option to disable XSS protection
### 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
See the [change log](CHANGELOG.md) for all of the updates.

View File

@@ -3,10 +3,10 @@ import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.36.1"
val ScalatraVersion = "2.7.1"
val JettyVersion = "9.4.43.v20210629"
val JgitVersion = "5.12.0.202106070339-r"
val GitBucketVersion = "4.37.1"
val ScalatraVersion = "2.8.2"
val JettyVersion = "9.4.44.v20210927"
val JgitVersion = "5.13.0.202109080827-r"
lazy val root = (project in file("."))
.enablePlugins(SbtTwirl, ScalatraPlugin)
@@ -15,7 +15,7 @@ sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.13.6"
scalaVersion := "2.13.7"
scalafmtOnCompile := true
@@ -34,7 +34,7 @@ libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "3.6.11" cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.0.3" cross CrossVersion.for3Use2_13,
"commons-io" % "commons-io" % "2.11.0",
"io.github.gitbucket" % "solidbase" % "1.0.3",
"io.github.gitbucket" % "markedj" % "1.0.16",
@@ -42,34 +42,34 @@ libraryDependencies ++= Seq(
"org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.8.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
"org.apache.sshd" % "apache-sshd" % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "2.0.0",
"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",
"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.3",
"org.postgresql" % "postgresql" % "42.2.23",
"ch.qos.logback" % "logback-classic" % "1.2.4",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4",
"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",
"net.coobird" % "thumbnailator" % "0.4.15",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.10.1",
"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.11.2" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.39.5" % "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.131" % "test"
"org.kohsuke" % "github-api" % "1.301" % "test"
)
libraryDependencies ~= {
@@ -200,7 +200,7 @@ executableKey := {
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName
IO jar (contentMappings.map { case (file, path) => (file, path.toString) }, outputFile, manifest)
IO jar (contentMappings.map { case (file, path) => (file, path.toString) }, outputFile, manifest, None)
// generate checksums
Seq(

View File

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

View File

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

View File

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

View File

@@ -20,15 +20,15 @@ public class PatchUtil {
public static String apply(String source, String patch, FileHeader fh)
throws IOException, PatchApplyException {
RawText rt = new RawText(source.getBytes("UTF-8"));
List<String> oldLines = new ArrayList<String>(rt.size());
List<String> oldLines = new ArrayList<>(rt.size());
for (int i = 0; i < rt.size(); i++)
oldLines.add(rt.getString(i));
List<String> newLines = new ArrayList<String>(oldLines);
List<String> newLines = new ArrayList<>(oldLines);
for (HunkHeader hh : fh.getHunks()) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset());
RawText hrt = new RawText(out.toByteArray());
List<String> hunkLines = new ArrayList<String>(hrt.size());
List<String> hunkLines = new ArrayList<>(hrt.size());
for (int i = 0; i < hrt.size(); i++)
hunkLines.add(hrt.getString(i));
int pos = 0;

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package gitbucket.core
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.sql.Connection
import java.util
import java.util.UUID
import gitbucket.core.model.Activity
@@ -84,7 +83,7 @@ object GitBucketCoreModule
new Version(
"4.34.0",
new Migration() {
override def migrate(moduleId: String, version: String, context: util.Map[String, AnyRef]): Unit = {
override def migrate(moduleId: String, version: String, context: java.util.Map[String, AnyRef]): Unit = {
implicit val formats: Formats = Serialization.formats(NoTypeHints)
import JDBCUtil._
@@ -119,5 +118,8 @@ object GitBucketCoreModule
new Version("4.35.2"),
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.1"),
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

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

View File

@@ -1,5 +1,49 @@
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

@@ -86,7 +86,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val newForm = mapping(
"userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password", text(required, maxlength(20)))),
"password" -> trim(label("Password", text(required, maxlength(40)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress()))),
"extraMailAddresses" -> list(
@@ -98,7 +98,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)(AccountNewForm.apply)
val editForm = mapping(
"password" -> trim(label("Password", optional(text(maxlength(20))))),
"password" -> trim(label("Password", optional(text(maxlength(40))))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))),
"extraMailAddresses" -> list(
@@ -434,7 +434,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { x =>
getAccountByUserName(userName).foreach { x =>
val (tokenId, token) = generateAccessToken(userName, form.note)
flash.update("generatedToken", (tokenId, token))
}

View File

@@ -242,25 +242,21 @@ trait PullRequestsControllerBase extends ControllerBase {
branchIsOutOfDate = JGitUtil.getShaByRef(repository.owner, repository.name, pullreq.branch) != Some(
pullreq.commitIdFrom
),
needStatusCheck = context.loginAccount
.map { u =>
branchProtection.needStatusCheck(u.userName)
}
.getOrElse(true),
needStatusCheck = context.loginAccount.forall { u =>
branchProtection.needStatusCheck(u.userName)
},
hasUpdatePermission = hasDeveloperRole(
pullreq.requestUserName,
pullreq.requestRepositoryName,
context.loginAccount
) &&
context.loginAccount
.map { u =>
!getProtectedBranchInfo(
pullreq.requestUserName,
pullreq.requestRepositoryName,
pullreq.requestBranch
).needStatusCheck(u.userName)
}
.getOrElse(false),
context.loginAccount.exists { u =>
!getProtectedBranchInfo(
pullreq.requestUserName,
pullreq.requestRepositoryName,
pullreq.requestBranch
).needStatusCheck(u.userName)
},
hasMergePermission = hasMergePermission,
commitIdTo = pullreq.commitIdTo
)
@@ -494,8 +490,7 @@ trait PullRequestsControllerBase extends ControllerBase {
(repository.userName, repository.repositoryName, repository.defaultBranch)
},
commits.flatten
.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false))
.flatten
.flatMap(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false))
.toList,
originId,
forkedId,

View File

@@ -138,7 +138,7 @@ trait ReleaseControllerBase extends ControllerBase {
get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository =>
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 commits = JGitUtil.getCommitLog(git, previousTagId, currentTag).reverse

View File

@@ -685,7 +685,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map {
objectId =>
if (raw) {
// Download (This route is left for backword compatibility)
// Download (This route is left for backward compatibility)
responseRawFile(git, objectId, path, repository)
} else {
val info = EditorConfigUtil.getEditorConfigInfo(git, id, path)

View File

@@ -1,7 +1,6 @@
package gitbucket.core.controller
import java.io.FileInputStream
import gitbucket.core.admin.html
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService._
@@ -50,8 +49,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
"ssh" -> mapping(
"enabled" -> trim(label("SSH access", boolean())),
"host" -> trim(label("SSH host", optional(text()))),
"port" -> trim(label("SSH port", optional(number())))
"bindAddress" -> mapping(
"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),
"useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked(
@@ -116,8 +127,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else None,
if (settings.ssh.enabled && settings.ssh.sshHost.isEmpty) {
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
if (settings.ssh.enabled && settings.ssh.bindAddress.isEmpty) {
Some("ssh.bindAddress.host" -> "SSH bind host is required if SSH access is enabled.")
} else None
).flatten
}
@@ -186,7 +197,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val newUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password", text(required, maxlength(20)))),
"password" -> trim(label("Password", text(required, maxlength(40)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress()))),
"extraMailAddresses" -> list(
@@ -200,7 +211,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val editUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier))),
"password" -> trim(label("Password", optional(text(maxlength(20))))),
"password" -> trim(label("Password", optional(text(maxlength(40))))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))),
"extraMailAddresses" -> list(
@@ -308,12 +319,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if (form.sshAddress != context.settings.sshAddress) {
if (form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress) {
SshServer.stop()
for {
sshAddress <- form.sshAddress
bindAddress <- form.ssh.bindAddress
publicAddress <- form.ssh.publicAddress.orElse(form.ssh.bindAddress)
baseUrl <- form.baseUrl
} SshServer.start(sshAddress, baseUrl)
} SshServer.start(bindAddress, publicAddress, baseUrl)
}
flash.update("info", "System settings has been updated.")
@@ -361,8 +373,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
})
get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val includeGroups = params.get("includeGroups").map(_.toBoolean).getOrElse(false)
val includeRemoved = params.get("includeRemoved").exists(_.toBoolean)
val includeGroups = params.get("includeGroups").exists(_.toBoolean)
val users = getAllUsers(includeRemoved, includeGroups)
val members = users.collect {
case account if (account.isGroupAccount) =>

View File

@@ -1,9 +1,10 @@
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.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.ReferrerAuthenticator
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{ReferrerAuthenticator, RepositoryName, WritableUsersAuthenticator}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.RefUpdate.Result
@@ -14,47 +15,38 @@ import scala.jdk.CollectionConverters._
import scala.util.Using
trait ApiGitReferenceControllerBase extends ControllerBase {
self: ReferrerAuthenticator =>
self: ReferrerAuthenticator with WritableUsersAuthenticator =>
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
* 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 =>
getRef()
val revstr = multiParams("splat").head
getRef(revstr, repository)
})
// Some versions of GHE support this path
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")
getRef()
})
private def getRef() = {
val revstr = multiParams("splat").head
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
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))
})
}
}
}
getRef(revstr, repository)
})
/*
* ii. Get all references
@@ -65,17 +57,17 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iii. 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 {
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)
if (ref == null) {
val update = git.getRepository.updateRef(data.ref)
update.setNewObjectId(ObjectId.fromString(data.sha))
val result = update.update()
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())
}
} else {
@@ -89,11 +81,11 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iv. 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("/")
extractFromJsonBody[UpdateARef].map {
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)
if (ref == null) {
UnprocessableEntity("Ref does not exist.")
@@ -104,7 +96,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
val result = update.update()
result match {
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())
}
}
@@ -116,7 +108,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* v. 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("/")
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
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

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

View File

@@ -121,7 +121,7 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[Seq[String]];
data <- extractFromJsonBody[Seq[String]]
issueId <- params("id").toIntOpt
} yield {
data.map { labelName =>
@@ -160,7 +160,7 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[Seq[String]];
data <- extractFromJsonBody[Seq[String]]
issueId <- params("id").toIntOpt
} yield {
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)

View File

@@ -2,7 +2,6 @@ package gitbucket.core.controller.api
import gitbucket.core.api._
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.MilestonesService
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import org.scalatra.NoContent
@@ -102,17 +101,4 @@ trait ApiIssueMilestoneControllerBase extends ControllerBase {
NoContent()
})
private def getApiMilestone(repository: RepositoryInfo, milestoneId: Int): Option[ApiMilestone] = {
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(p => p._1.milestoneId == milestoneId)
.map(
milestoneWithIssue =>
ApiMilestone(
repository.repository,
milestoneWithIssue._1,
milestoneWithIssue._2,
milestoneWithIssue._3
)
)
}
}

View File

@@ -86,7 +86,7 @@ trait ApiReleaseControllerBase extends ControllerBase {
/**
* vi. Edit a release
* https://developer.github.com/v3/repos/releases/#edit-a-release
* Incompatiblity info: GitHub API requires :release_id, but GitBucket API requires :tag_name
* Incompatibility info: GitHub API requires :release_id, but GitBucket API requires :tag_name
*/
patch("/api/v3/repos/:owner/:repository/releases/:tag")(writableUsersOnly { repository =>
(for {
@@ -103,7 +103,7 @@ trait ApiReleaseControllerBase extends ControllerBase {
/**
* vii. Delete a release
* https://developer.github.com/v3/repos/releases/#delete-a-release
* Incompatiblity info: GitHub API requires :release_id, but GitBucket API requires :tag_name
* Incompatibility info: GitHub API requires :release_id, but GitBucket API requires :tag_name
*/
delete("/api/v3/repos/:owner/:repository/releases/:tag")(writableUsersOnly { repository =>
val tag = params("tag")

View File

@@ -40,7 +40,7 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, commitInfo.id.toString).size
commentCount = getCommitComment(repository.owner, repository.name, commitInfo.id).size
)
})
}

View File

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

View File

@@ -1,14 +1,15 @@
package gitbucket.core.plugin
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import io.github.gitbucket.solidbase.model.Version
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.command.Command
import play.twirl.api.Html
import scala.util.Using
/**
@@ -323,7 +324,7 @@ abstract class Plugin {
/**
* 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.
@@ -332,7 +333,7 @@ abstract class Plugin {
registry: PluginRegistry,
context: ServletContext,
settings: SystemSettings
): Seq[PartialFunction[String, Command]] = Nil
): Seq[PartialFunction[String, ChannelSession => Command]] = Nil
/**
* 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.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentHashMap
import javax.servlet.ServletContext
import com.github.zafarkhaja.semver.Version
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.model.Module
import org.apache.commons.io.FileUtils
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.command.Command
import org.slf4j.LoggerFactory
import play.twirl.api.Html
@@ -58,7 +58,7 @@ class PluginRegistry {
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
suggestionProviders.add(new UserNameSuggestionProvider())
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)
@@ -177,10 +177,11 @@ class PluginRegistry {
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)
def getSshCommandProviders: Seq[PartialFunction[String, Command]] = sshCommandProviders.asScala.toSeq
def getSshCommandProviders: Seq[PartialFunction[String, ChannelSession => Command]] =
sshCommandProviders.asScala.toSeq
}
/**

View File

@@ -47,7 +47,7 @@ trait AccountService {
case _ => None
}
case account if (!account.isGroupAccount && account.password == sha1(password)) => Some(account)
} getOrElse None
}.flatten
}
/**

View File

@@ -39,9 +39,7 @@ trait ActivityService {
if (isPublic == false) {
list += activity
} else {
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName).forall(_.isPrivate)) {
list += activity
}
}
@@ -61,9 +59,7 @@ trait ActivityService {
var json: String = null
while (list.length < 50 && { json = reader.readLine(); json } != null) {
val activity = read[Activity](json)
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName).forall(_.isPrivate)) {
list += activity
}
}
@@ -83,9 +79,7 @@ trait ActivityService {
val activity = read[Activity](json)
if (owners.contains(activity.userName)) {
list += activity
} else if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
} else if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName).forall(_.isPrivate)) {
list += activity
}
}

View File

@@ -336,13 +336,16 @@ trait IssuesService {
implicit s: Session
) =
Issues filter { t1 =>
(if (repos.size == 1) {
(if (repos.sizeIs == 1) {
t1.byRepository(repos.head._1, repos.head._2)
} else {
((t1.userName ++ "/" ++ t1.repositoryName) inSetBind (repos.map { case (owner, repo) => s"$owner/$repo" }))
}) &&
(t1.closed === (condition.state == "closed").bind)
.&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
(condition.state match {
case "open" => t1.closed === false
case "closed" => t1.closed === true
case _ => t1.closed === true || t1.closed === false
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
.&&(t1.priorityId.? isEmpty, condition.priority == Some(None))
.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
.&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
@@ -939,7 +942,7 @@ object IssuesService {
case x => Some(x)
},
param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "state", Seq("open", "closed", "all")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),
@@ -960,7 +963,7 @@ object IssuesService {
case x => Some(x)
},
param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "state", Seq("open", "closed", "all")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),

View File

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

View File

@@ -71,7 +71,7 @@ object ProtectedBranchService {
pusher: String,
mergePullRequest: Boolean
)(implicit session: Session): Option[String] = {
if (mergePullRequest == true) {
if (mergePullRequest) {
None
} else {
checkBranchProtection(owner, repository, receivePack, command, pusher)
@@ -153,9 +153,9 @@ object ProtectedBranchService {
Some("Cannot force-push to a protected branch")
case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
unSuccessedContexts(command.getNewId.name) match {
case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
case _ => None
case s if s.sizeIs == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
case s if s.sizeIs >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
case _ => None
}
case ReceiveCommand.Type.DELETE =>
Some("Cannot delete a protected branch")

View File

@@ -509,10 +509,11 @@ trait PullRequestService {
def getPullRequestComments(userName: String, repositoryName: String, issueId: Int, commits: Seq[CommitInfo])(
implicit s: Session
): Seq[Comment] = {
(commits
.map(commit => getCommitComments(userName, repositoryName, commit.id, true))
.flatten ++ getComments(userName, repositoryName, issueId))
.groupBy {
(commits.flatMap(commit => getCommitComments(userName, repositoryName, commit.id, true)) ++ getComments(
userName,
repositoryName,
issueId
)).groupBy {
case x: IssueComment => (Some(x.commentId), None, None, None)
case x: CommitComment if x.fileName.isEmpty => (Some(x.commentId), None, None, None)
case x: CommitComment => (None, x.fileName, x.originalOldLine, x.originalNewLine)
@@ -578,7 +579,7 @@ trait PullRequestService {
case (oldGit, newGit) =>
if (originRepository.branchList.contains(originId)) {
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(
oldGit,
@@ -595,9 +596,9 @@ trait PullRequestService {
} else {
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 =
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)))
}

View File

@@ -25,7 +25,7 @@ object RepositoryCreationService {
private val Creating = new ConcurrentHashMap[String, Option[String]]()
def isCreating(owner: String, repository: String): Boolean = {
Option(Creating.get(s"${owner}/${repository}")).map(_.isEmpty).getOrElse(false)
Option(Creating.get(s"${owner}/${repository}")).exists(_.isEmpty)
}
def startCreation(owner: String, repository: String): Unit = {
@@ -40,7 +40,7 @@ object RepositoryCreationService {
}
def getCreationError(owner: String, repository: String): Option[String] = {
Option(Creating.remove(s"${owner}/${repository}")).getOrElse(None)
Option(Creating.remove(s"${owner}/${repository}")).flatten
}
}

View File

@@ -12,6 +12,7 @@ import gitbucket.core.util.JGitUtil.FileInfo
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{Repository => _}
import scala.util.Using
trait RepositoryService {
@@ -766,7 +767,8 @@ trait RepositoryService {
JGitUtil.getContentFromId(git, file.id, true).collect {
case bytes if FileUtil.isText(bytes) => StringUtil.convertFromByteArray(bytes)
}
} getOrElse None
}
.flatten
} getOrElse ""
}
}
@@ -834,12 +836,10 @@ object RepositoryService {
def httpUrl(owner: String, name: String)(implicit context: Context): String =
s"${context.baseUrl}/git/${owner}/${name}.git"
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
if (context.settings.ssh.enabled) {
context.settings.sshAddress.map { x =>
s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git"
}
} else None
context.settings.sshUrl(owner, name)
def openRepoUrl(openUrl: String)(implicit context: Context): String =
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.oauth2.sdk.auth.Secret
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.Directory._
import scala.util.Using
trait SystemSettingsService {
@@ -29,8 +30,14 @@ trait SystemSettingsService {
props.setProperty(Notification, settings.notification.toString)
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
props.setProperty(SshEnabled, settings.ssh.enabled.toString)
settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
settings.ssh.bindAddress.foreach { bindAddress =>
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)
if (settings.useSMTP) {
settings.smtp.foreach { smtp =>
@@ -72,7 +79,7 @@ trait SystemSettingsService {
}
}
}
props.setProperty(SkinName, settings.skinName.toString)
props.setProperty(SkinName, settings.skinName)
settings.userDefinedCss.foreach(x => props.setProperty(UserDefinedCss, x))
props.setProperty(ShowMailAddress, settings.showMailAddress.toString)
props.setProperty(WebHookBlockPrivateAddress, settings.webHook.blockPrivateAddress.toString)
@@ -95,6 +102,10 @@ trait SystemSettingsService {
props.load(in)
}
}
loadSystemSettings(props)
}
def loadSystemSettings(props: java.util.Properties): SystemSettings = {
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue(props, Information, None),
@@ -112,9 +123,20 @@ trait SystemSettingsService {
getValue(props, Notification, false),
getValue(props, LimitVisibleRepositories, false),
Ssh(
getValue(props, SshEnabled, false),
getOptionValue[String](props, SshHost, None).map(_.trim),
getOptionValue(props, SshPort, Some(DefaultSshPort))
enabled = getValue(props, SshEnabled, false),
bindAddress = {
// 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(
props,
@@ -182,7 +204,6 @@ trait SystemSettingsService {
)
)
}
}
object SystemSettingsService {
@@ -214,7 +235,6 @@ object SystemSettingsService {
upload: Upload,
repositoryViewer: RepositoryViewerSettings
) {
def baseUrl(request: HttpServletRequest): String =
baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
@@ -231,11 +251,17 @@ object SystemSettingsService {
.fold(base)(_ + base.dropWhile(_ != ':'))
}
def sshAddress: Option[SshAddress] =
ssh.sshHost.collect {
case host if ssh.enabled =>
SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git")
}
def sshBindAddress: Option[SshAddress] =
ssh.bindAddress
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(
@@ -248,9 +274,35 @@ object SystemSettingsService {
case class Ssh(
enabled: Boolean,
sshHost: Option[String],
sshPort: Option[Int]
)
bindAddress: Option[SshAddress],
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(
host: String,
@@ -296,7 +348,25 @@ object SystemSettingsService {
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])
@@ -304,6 +374,8 @@ object SystemSettingsService {
case class RepositoryViewerSettings(maxFiles: Int)
val GenericSshUser = "git"
val PublicSshPort = 22
val DefaultSshPort = 29418
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
@@ -325,6 +397,10 @@ object SystemSettingsService {
private val SshEnabled = "ssh"
private val SshHost = "ssh.host"
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 SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port"

View File

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

View File

@@ -75,13 +75,15 @@ trait WikiService {
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
if (!JGitUtil.isEmpty(git)) {
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
val fileName = pageName + ".md"
JGitUtil.getLatestCommitFromPath(git, fileName, "master").map { latestCommit =>
val content = JGitUtil.getContentFromPath(git, latestCommit.getTree, fileName, true)
WikiPageInfo(
file.name,
StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
file.author,
file.time,
file.commitId
fileName,
StringUtil.convertFromByteArray(content.getOrElse(Array.empty)),
latestCommit.getAuthorIdent.getName,
latestCommit.getAuthorIdent.getWhen,
latestCommit.getName
)
}
} else None
@@ -145,7 +147,7 @@ trait WikiService {
if (!p.getErrors.isEmpty) {
throw new PatchFormatException(p.getErrors())
}
val revertInfo = (p.getFiles.asScala.map { fh =>
val revertInfo = p.getFiles.asScala.flatMap { fh =>
fh.getChangeType match {
case DiffEntry.ChangeType.MODIFY => {
val source =
@@ -174,7 +176,7 @@ trait WikiService {
}
case _ => Nil
}
}).flatten
}
if (revertInfo.nonEmpty) {
val builder = DirCache.newInCore.builder()
@@ -255,8 +257,7 @@ trait WikiService {
created = false
updated = JGitUtil
.getContentFromId(git, tree.getEntryObjectId, true)
.map(new String(_, "UTF-8") != content)
.getOrElse(false)
.exists(new String(_, "UTF-8") != content)
}
}
}

View File

@@ -139,7 +139,7 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
case _ =>
() =>
{
logger.debug(s"Not enough path arguments: ${request.paths}")
logger.debug(s"Not enough path arguments: ${request.paths.mkString(", ")}")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}

View File

@@ -3,7 +3,6 @@ package gitbucket.core.servlet
import java.io.File
import java.util
import java.util.Date
import scala.util.Using
import gitbucket.core.api
import gitbucket.core.api.JsonFormat.Context
@@ -209,9 +208,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
val settings = loadSystemSettings()
val baseUrl = settings.baseUrl(request)
val sshUrl = settings.sshAddress.map { x =>
s"${x.genericUser}@${x.host}:${x.port}"
}
val sshUrl = settings.sshUrl(owner, repository)
if (!repository.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
@@ -347,9 +344,9 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// set PR as merged
val pulls = getPullRequestsByBranch(owner, repository, branchName, Some(false))
pulls.foreach { pull =>
if (commits.find { c =>
if (commits.exists { c =>
c.id == pull.commitIdTo
}.isDefined) {
}) {
markMergeAndClosePullRequest(pusher, owner, repository, pull)
getAccountByUserName(pusher).foreach { pusherAccount =>
callPullRequestWebHook("closed", repositoryInfo, pull.issueId, pusherAccount, settings)

View File

@@ -5,26 +5,31 @@ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService}
import gitbucket.core.servlet.{CommitLogHook, Database}
import gitbucket.core.util.Directory
import org.apache.sshd.server.{Environment, ExitCallback, SessionAware}
import org.apache.sshd.server.{Environment, ExitCallback}
import org.apache.sshd.server.command.{Command, CommandFactory}
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.server.session.{ServerSession, ServerSessionAware}
import org.slf4j.LoggerFactory
import java.io.{File, InputStream, OutputStream}
import java.io.{File, InputStream, OutputStream}
import org.eclipse.jgit.api.Git
import Directory._
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
import org.apache.sshd.server.channel.ChannelSession
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.shell.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
import scala.util.Using
object GitCommand {
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 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])
@@ -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())
thread.start()
}
override def destroy(): Unit = {}
override def destroy(channel: ChannelSession): Unit = {}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
@@ -144,11 +149,9 @@ class DefaultGitUploadPack(owner: String, repoName: String)
override protected def runTask(authType: AuthType): Unit = {
val execute = Database() withSession { implicit session =>
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""))
.map { repositoryInfo =>
!repositoryInfo.repository.isPrivate || isReadableUser(authType, repositoryInfo)
}
.getOrElse(false)
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).exists { repositoryInfo =>
!repositoryInfo.repository.isPrivate || isReadableUser(authType, repositoryInfo)
}
}
if (execute) {
@@ -161,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)
with RepositoryService
with AccountService
@@ -169,11 +172,9 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, ss
override protected def runTask(authType: AuthType): Unit = {
val execute = Database() withSession { implicit session =>
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""))
.map { repositoryInfo =>
isWritableUser(authType, repositoryInfo)
}
.getOrElse(false)
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).exists { repositoryInfo =>
isWritableUser(authType, repositoryInfo)
}
}
if (execute) {
@@ -181,7 +182,8 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, ss
val repository = git.getRepository
val receive = new ReceivePack(repository)
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.setPostReceiveHook(hook)
}
@@ -231,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])
override def createCommand(command: String): Command = {
override def createCommand(channel: ChannelSession, command: String): Command = {
import GitCommand._
logger.debug(s"command: $command")
@@ -242,19 +244,24 @@ class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends Command
case f if f.isDefinedAt(command) => f(command)
}
pluginCommand match {
case Some(x) => x
case None =>
command match {
case SimpleCommandRegex("upload", repoName) if (pluginRepository(repoName)) =>
new PluginGitUploadPack(repoName, routing(repoName))
case SimpleCommandRegex("receive", repoName) if (pluginRepository(repoName)) =>
new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload", owner, repoName) => new DefaultGitUploadPack(owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) =>
new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
case _ => new UnknownCommand(command)
pluginCommand.map(_.apply(channel)).getOrElse {
val (simpleRegex, defaultRegex) =
if (sshAddress.isDefaultPort) {
(SimpleCommandRegexPort22, DefaultCommandRegexPort22)
} else {
(SimpleCommandRegex, DefaultCommandRegex)
}
command match {
case simpleRegex("upload", repoName) if pluginRepository(repoName) =>
new PluginGitUploadPack(repoName, routing(repoName))
case simpleRegex("receive", repoName) if pluginRepository(repoName) =>
new PluginGitReceivePack(repoName, routing(repoName))
case defaultRegex("upload", owner, repoName) =>
new DefaultGitUploadPack(owner, repoName)
case defaultRegex("receive", owner, repoName) =>
new DefaultGitReceivePack(owner, repoName, baseUrl, sshAddress)
case _ => new UnknownCommand(command)
}
}
}

View File

@@ -1,20 +1,23 @@
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 =
"""
| Welcome to
@@ -30,8 +33,8 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
|
| Please use:
|
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
| git clone %s
""".stripMargin.format(placeholderAddress).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message))
err.flush()
in.close()
@@ -40,7 +43,7 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
callback.onExit(127)
}
override def destroy(): Unit = {}
override def destroy(channel: ChannelSession): Unit = {}
override def setInputStream(in: InputStream): Unit = {
this.in = in

View File

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

View File

@@ -1,8 +1,7 @@
package gitbucket.core.ssh
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.Directory
@@ -11,40 +10,48 @@ import org.slf4j.LoggerFactory
object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private val server = new AtomicReference[org.apache.sshd.server.SshServer](null)
private def configure(sshAddress: SshAddress, baseUrl: String) = {
server.setPort(sshAddress.port)
private def configure(
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(
java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser")
)
provider.setAlgorithm("RSA")
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(bindAddress.genericUser))
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) = {
if (active.compareAndSet(false, true)) {
configure(sshAddress, baseUrl)
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
def start(bindAddress: SshAddress, publicAddress: SshAddress, baseUrl: String): Unit = {
this.server.synchronized {
val server = configure(bindAddress, publicAddress, baseUrl)
if (this.server.compareAndSet(null, server)) {
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
}
}
def stop() = {
if (active.compareAndSet(true, false)) {
server.stop(true)
logger.info("SSH Server is stopped.")
def stop(): Unit = {
this.server.synchronized {
val server = this.server.getAndSet(null)
if (server != null) {
server.stop()
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 = {
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.")
}
for {
sshAddress <- settings.sshAddress
bindAddress <- settings.sshBindAddress
publicAddress <- settings.sshPublicAddress
baseUrl <- settings.baseUrl
} SshServer.start(sshAddress, baseUrl)
} SshServer.start(bindAddress, publicAddress, baseUrl)
}
override def contextDestroyed(sce: ServletContextEvent): Unit = {

View File

@@ -38,7 +38,7 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
case Some(x) if (repository.owner == x.userName) => action(repository)
// TODO Repository management is allowed for only group managers?
case Some(x) if (getGroupMembers(repository.owner).exists { m =>
m.userName == x.userName && m.isManager == true
m.userName == x.userName && m.isManager
}) =>
action(repository)
case Some(x) if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName)) =>

View File

@@ -52,7 +52,7 @@ object EditorConfigUtil {
}
override def getParent: ResourcePath = {
Option(path.getParentPath).map { new JGitResourcePath(repo, revStr, _) }.getOrElse(null)
Option(path.getParentPath).map { new JGitResourcePath(repo, revStr, _) }.orNull
}
override def openRandomReader(): Resource.RandomReader = {
@@ -70,7 +70,7 @@ object EditorConfigUtil {
private class JGitResourcePath(repo: Repository, revStr: String, path: Ec4jPath) extends ResourcePath {
override def getParent: ResourcePath = {
Option(path.getParentPath).map { new JGitResourcePath(repo, revStr, _) }.getOrElse(null)
Option(path.getParentPath).map { new JGitResourcePath(repo, revStr, _) }.orNull
}
override def getPath: Ec4jPath = {

View File

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

View File

@@ -227,7 +227,7 @@ object JDBCUtil {
if (noPreds.isEmpty) {
if (hasPreds.isEmpty) done else sys.error(hasPreds.toString)
} else {
val found = noPreds.map { _._1 }
val found = noPreds.keys
tsort(hasPreds.map { case (k, v) => (k, v -- found) }, done ++ found)
}
}

View File

@@ -37,9 +37,8 @@ object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] = new Releasable[ObjectDatabase] {
override def release(resource: ObjectDatabase): Unit = resource.close()
}
implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
_.close()
/**
* The repository data.
@@ -229,10 +228,11 @@ object JGitUtil {
*
* @param name the tag name
* @param time the tagged date
* @param id the commit id
* @param commitId the commit id
* @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
@@ -348,7 +348,8 @@ object JGitUtil {
ref.getName.stripPrefix("refs/tags/"),
revCommit.getCommitterIdent.getWhen,
revCommit.getName,
revCommit.getShortMessage
revCommit.getShortMessage,
ref.getObjectId.getName
)
)
} catch {
@@ -383,7 +384,7 @@ object JGitUtil {
path: String = ".",
baseUrl: Option[String] = None,
commitCount: Int = 0,
maxFiles: Int = 100
maxFiles: Int = 5
): List[FileInfo] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val objectId = git.getRepository.resolve(revision)
@@ -659,9 +660,13 @@ object JGitUtil {
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
paths.map { path =>
paths.flatMap { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
(path, commit)
if (commit == null) {
None
} else {
Some((path, commit))
}
}.toMap
}
@@ -672,11 +677,10 @@ object JGitUtil {
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
getDiffEntries(git, from, to)
.map { entry =>
.foreach { entry =>
df.format(entry)
new String(out.toByteArray, "UTF-8")
}
.mkString("\n")
new String(out.toByteArray, "UTF-8")
}
private def getDiffEntries(git: Git, from: Option[String], to: String): Seq[DiffEntry] = {
@@ -1259,7 +1263,7 @@ object JGitUtil {
val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]()
0.to(blame.getResultContents().size() - 1).map { i =>
0.until(blame.getResultContents().size()).foreach { i =>
val c = blame.getSourceCommit(i)
if (!blameMap.contains(c.name)) {
blameMap += c.name -> JGitUtil.BlameInfo(

View File

@@ -41,7 +41,7 @@ class Mailer(settings: SystemSettings) {
htmlMsg: Option[String] = None,
loginAccount: Option[Account] = None
): Option[HtmlEmail] = {
if (settings.notification == true) {
if (settings.notification) {
settings.smtp.map { smtp =>
val email = new HtmlEmail
email.setHostName(smtp.host)
@@ -51,7 +51,7 @@ class Mailer(settings: SystemSettings) {
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
if (ssl == true) {
if (ssl) {
email.setSslSmtpPort(smtp.port.get.toString)
}
}

View File

@@ -184,8 +184,7 @@ object StringUtil {
def removeUserName(baseUrl: String): String = baseUrl.replaceFirst("(https?://).+@", "$1")
gitRepositoryUrl match {
case GitBucketUrlPattern(base, user, repository)
if baseUrl.map(removeUserName(base).startsWith).getOrElse(false) =>
case GitBucketUrlPattern(base, user, repository) if baseUrl.exists(removeUserName(base).startsWith) =>
s"${removeUserName(base)}/$user/$repository"
case GitHubUrlPattern(_, user, repository) => s"https://github.com/$user/$repository"
case BitBucketUrlPattern(_, user, repository) => s"https://bitbucket.org/$user/$repository"

View File

@@ -45,11 +45,14 @@ trait AvatarImageProvider { self: RequestCache =>
if (tooltip) {
Html(
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}" alt="@${userName}" />"""
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;"
| alt="@${StringUtil.escapeHtml(userName)}"
| data-toggle="tooltip" title="${StringUtil.escapeHtml(userName)}" />""".stripMargin
)
} else {
Html(
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" alt="@${userName}" />"""
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;"
| alt="@${StringUtil.escapeHtml(userName)}" />""".stripMargin
)
}
}

View File

@@ -336,7 +336,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
)(implicit context: Context): Html = {
val avatarHtml = avatar(userName, size, tooltip, mailAddress)
val contentHtml = if (label == true) Html(avatarHtml.body + " " + userName) else avatarHtml
val contentHtml = if (label) Html(avatarHtml.body + " " + userName) else avatarHtml
userWithContent(userName, mailAddress)(contentHtml)
}
@@ -446,14 +446,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
result.append(c)
}
case '>' if tag == true => {
case '>' if tag => {
tag = false
result.append(c)
}
case _ if tag == false => {
text.append(c)
}
case _ if tag == true => {
case _ if tag => {
result.append(c)
}
}

View File

@@ -1,18 +1,18 @@
@(account: gitbucket.core.model.Account,
personalTokens: List[gitbucket.core.model.AccessToken],
gneratedToken: Option[(gitbucket.core.model.AccessToken, String)])(implicit context: gitbucket.core.controller.Context)
generatedToken: Option[(gitbucket.core.model.AccessToken, String)])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Applications"){
@gitbucket.core.account.html.menu("application", context.loginAccount.get.userName, false){
<div class="panel panel-default">
<div class="panel-heading strong">Personal access tokens</div>
<div class="panel-body">
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
@if(personalTokens.isEmpty && generatedToken.isEmpty){
No tokens.
} else {
Tokens you have generated which can be used to access the GitBucket API.
<hr style="margin-top: 10px;">
}
@gneratedToken.map { case (token, tokenString) =>
@generatedToken.map { case (token, tokenString) =>
<div class="alert alert-info">
Make sure to copy your new personal access token now. You won't be able to see it again!
</div>

View File

@@ -19,22 +19,40 @@
<label class="checkbox">
<input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
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>
</fieldset>
<div class="ssh">
<div class="form-group">
<label class="control-label col-md-2" for="sshHost">SSH host</label>
<div class="col-md-10">
<input type="text" id="sshHost" name="ssh.host" class="form-control" value="@context.settings.ssh.sshHost"/>
<span id="error-ssh_host" class="error"></span>
<div class="bindAddress">
<div class="form-group">
<label class="control-label col-md-2" for="sshBindHost">SSH bind host</label>
<div class="col-md-10">
<input type="text" id="sshBindHost" name="ssh.bindAddress.host" class="form-control" value="@context.settings.ssh.bindAddress.map(_.host)"/>
<span id="error-ssh_bindAddress_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshBindPort">SSH bind port</label>
<div class="col-md-10">
<input type="text" id="sshBindPort" name="ssh.bindAddress.port" class="form-control" value="@context.settings.ssh.bindAddress.map(_.port)"/>
<span id="error-ssh_bindAddress_port" class="error"></span>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshPort">SSH port</label>
<div class="col-md-10">
<input type="text" id="sshPort" name="ssh.port" class="form-control" value="@context.settings.ssh.sshPort"/>
<span id="error-ssh_port" class="error"></span>
<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>

View File

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

View File

@@ -16,7 +16,7 @@
<td>
<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)/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>
</div>
<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 =>
@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)/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>
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 =
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.
// See the following: https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142
val statisticsHandler = new StatisticsHandler

View File

@@ -2,10 +2,13 @@ package gitbucket.core.api
import gitbucket.core.TestingGitBucketServer
import org.apache.commons.io.IOUtils
import org.eclipse.jgit.api.Git
import org.scalatest.funsuite.AnyFunSuite
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.
@@ -134,6 +137,27 @@ class ApiIntegrationTest extends AnyFunSuite {
assert(statusList.get(1).getState == GHCommitState.FAILURE)
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")
}
}
}
@@ -150,7 +174,7 @@ class ApiIntegrationTest extends AnyFunSuite {
.content("create")
.message("Create content")
.path("README.md")
.commit();
.commit()
assert(createResult.getContent.isFile == true)
assert(IOUtils.toString(createResult.getContent.read(), "UTF-8") == "create")
@@ -168,7 +192,7 @@ class ApiIntegrationTest extends AnyFunSuite {
.message("Update content")
.path("README.md")
.sha(content1.getSha)
.commit();
.commit()
assert(updateResult.getContent.isFile == true)
assert(IOUtils.toString(updateResult.getContent.read(), "UTF-8") == "update")

View File

@@ -83,8 +83,20 @@ object ApiSpecModels {
milestoneCount = 1,
branchList = Seq("master", "develop"),
tags = Seq(
TagInfo(name = "v1.0", time = date("2015-05-05T23:40:27Z"), id = "id1", message = "1.0 released"),
TagInfo(name = "v2.0", time = date("2016-05-05T23:40:27Z"), id = "id2", message = "2.0 released")
TagInfo(
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")
)
@@ -177,6 +189,16 @@ object ApiSpecModels {
updatedDate = date1
)
val milestone = Milestone(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
milestoneId = 1,
title = "Test milestone",
description = Some("Milestone description"),
dueDate = Some(date1),
closedDate = Some(date1)
)
// APIs
val apiUser = ApiUser(account)
@@ -193,12 +215,20 @@ object ApiSpecModels {
repositoryName = repo1Name
)
val apiMilestone = ApiMilestone(
repository = repository,
milestone = milestone,
open_issue_count = 1,
closed_issue_count = 1
)
val apiIssue = ApiIssue(
issue = issue,
repositoryName = repo1Name,
user = apiUser,
assignee = Some(apiUser),
labels = List(apiLabel)
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
val apiNotAssignedIssue = ApiIssue(
@@ -206,7 +236,8 @@ object ApiSpecModels {
repositoryName = repo1Name,
user = apiUser,
assignee = None,
labels = List(apiLabel)
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
val apiIssuePR = ApiIssue(
@@ -214,7 +245,8 @@ object ApiSpecModels {
repositoryName = repo1Name,
user = apiUser,
assignee = Some(apiUser),
labels = List(apiLabel)
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
val apiComment = ApiComment(
@@ -412,9 +444,29 @@ object ApiSpecModels {
val apiPusher = ApiPusher(account)
val apiRef = ApiRef(
ref = "refs/heads/featureA",
`object` = ApiObject(sha1)
//have both urls as https, as the expected samples are using https
val gitHubContext = JsonFormat.Context("https://api.github.com", Some("https://api.github.com"))
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"
@@ -471,6 +523,19 @@ object ApiSpecModels {
val jsonLabel =
"""{"name":"bug","color":"f29513","url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/labels/bug"}"""
val jsonMilestone = """{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/milestones/1",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/milestone/1",
|"id":1,
|"number":1,
|"state":"closed",
|"title":"Test milestone",
|"description":"Milestone description",
|"open_issues":1,"closed_issues":1,
|"closed_at":"2011-04-14T16:00:49Z",
|"due_on":"2011-04-14T16:00:49Z"
|}""".stripMargin
val jsonIssue = s"""{
|"number":1347,
|"title":"Found a bug",
@@ -481,6 +546,7 @@ object ApiSpecModels {
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[$jsonUser],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
@@ -496,6 +562,7 @@ object ApiSpecModels {
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
@@ -512,6 +579,7 @@ object ApiSpecModels {
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"body":"Please pull these awesome changes",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[$jsonUser],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
@@ -729,8 +797,33 @@ object ApiSpecModels {
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 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 =
s"""{
|"name":"release.zip",

View File

@@ -8,6 +8,12 @@ class JsonFormatSpec extends AnyFunSuite {
implicit val format = JsonFormat.jsonFormats
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") {
assert(JsonFormat(apiUser) == expected(jsonUser))
@@ -76,8 +82,11 @@ class JsonFormatSpec extends AnyFunSuite {
test("apiPusher") {
assert(JsonFormat(apiPusher) == expected(jsonPusher))
}
test("apiRef") {
assert(JsonFormat(apiRef) == expected(jsonRef))
test("apiRefHead") {
assertEqualJson(JsonFormat(apiRefHeadsMaster)(gitHubContext), jsonRefHeadsMaster)
}
test("apiRefTag") {
assertEqualJson(JsonFormat(apiRefTag)(gitHubContext), jsonRefTag)
}
test("apiReleaseAsset") {
assert(JsonFormat(apiReleaseAsset) == expected(jsonReleaseAsset))

View File

@@ -47,8 +47,8 @@ trait ServiceSpecBase {
limitVisibleRepositories = false,
ssh = Ssh(
enabled = false,
sshHost = None,
sshPort = None
bindAddress = None,
publicAddress = None
),
useSMTP = false,
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

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

View File

@@ -1,38 +1,102 @@
package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.shell.UnknownCommand
import org.scalatest.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)
describe("createCommand") {
it("should return GitReceivePack when command is git-receive-pack") {
assert(factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack] == true)
assert(
factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack] == true
)
}
it("should return GitUploadPack when command is git-upload-pack") {
assert(factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[DefaultGitUploadPack] == true)
assert(factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[DefaultGitUploadPack] == true)
}
it("should return UnknownCommand when command is not git-(upload|receive)-pack") {
assert(factory.createCommand("git- '/owner/repo.git'").isInstanceOf[UnknownCommand] == true)
assert(factory.createCommand("git-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] == true)
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)
}
it("should return UnknownCommand when git command has no valid arguments") {
// must be: git-upload-pack '/owner/repository_name.git'
assert(factory.createCommand("git-upload-pack").isInstanceOf[UnknownCommand] == true)
assert(factory.createCommand("git-upload-pack /owner/repo.git").isInstanceOf[UnknownCommand] == true)
assert(factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand] == true)
assert(factory.createCommand("git-upload-pack '/ownerrepo.git'").isInstanceOf[UnknownCommand] == true)
assert(factory.createCommand("git-upload-pack '/owner/repo.wiki'").isInstanceOf[UnknownCommand] == true)
}
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"))
}
"createCommand" when {
val channel = new ChannelSession()
"receiving a git-receive-pack command" should {
"return DefaultGitReceivePack" when {
"the path matches owner/repo" in new MockContext {
assert(
factory.createCommand(channel, "git-receive-pack '/owner/repo.git'").isInstanceOf[DefaultGitReceivePack]
)
assert(
factory
.createCommand(channel, "git-receive-pack '/owner/repo.wiki.git'")
.isInstanceOf[DefaultGitReceivePack]
)
}
"the leading slash is left off and running on port 22" in new MockContext {
override val sshPort: Int = 22
assert(
factory.createCommand(channel, "git-receive-pack 'owner/repo.git'").isInstanceOf[DefaultGitReceivePack]
)
assert(
factory.createCommand(channel, "git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[DefaultGitReceivePack]
)
}
}
"return UnknownCommand" when {
"the ssh port is not 22 and the leading slash is missing" in new MockContext {
override val sshPort: Int = 1337
assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack 'owner/repo.wiki.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack 'oranges.git'").isInstanceOf[UnknownCommand])
assert(factory.createCommand(channel, "git-receive-pack 'apples.git'").isInstanceOf[UnknownCommand])
}
"the path is malformed" in new MockContext {
assert(
factory.createCommand(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

@@ -35,7 +35,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
assert(
provider.toHtml("user", 32).toString ==
"""<img src="https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e?s=32&d=retro&r=g" class="avatar" style="width: 32px; height: 32px;" alt="@user" />"""
"""<img src="https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e?s=32&d=retro&r=g" class="avatar" style="width: 32px; height: 32px;"
| alt="@user" />""".stripMargin
)
}
@@ -47,7 +48,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
assert(
provider.toHtml("user", 32).toString ==
s"""<img src="/user/_avatar?${date}" class="avatar" style="width: 32px; height: 32px;" alt="@user" />"""
s"""<img src="/user/_avatar?${date}" class="avatar" style="width: 32px; height: 32px;"
| alt="@user" />""".stripMargin
)
}
@@ -59,7 +61,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
assert(
provider.toHtml("user", 32).toString ==
s"""<img src="/user/_avatar?${date}" class="avatar" style="width: 32px; height: 32px;" alt="@user" />"""
s"""<img src="/user/_avatar?${date}" class="avatar" style="width: 32px; height: 32px;"
| alt="@user" />""".stripMargin
)
}
@@ -69,7 +72,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
assert(
provider.toHtml("user", 20, "hoge@hoge.com").toString ==
"""<img src="https://www.gravatar.com/avatar/4712f9b0e63f56ad952ad387eaa23b9c?s=20&d=retro&r=g" class="avatar-mini" style="width: 20px; height: 20px;" alt="@user" />"""
"""<img src="https://www.gravatar.com/avatar/4712f9b0e63f56ad952ad387eaa23b9c?s=20&d=retro&r=g" class="avatar-mini" style="width: 20px; height: 20px;"
| alt="@user" />""".stripMargin
)
}
@@ -79,7 +83,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
assert(
provider.toHtml("user", 20).toString ==
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;" alt="@user" />"""
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;"
| alt="@user" />""".stripMargin
)
}
@@ -89,7 +94,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
assert(
provider.toHtml("user", 20, "hoge@hoge.com").toString ==
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;" alt="@user" />"""
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;"
| alt="@user" />""".stripMargin
)
}
@@ -99,7 +105,27 @@ class AvatarImageProviderSpec extends AnyFunSpec {
assert(
provider.toHtml("user", 20, "hoge@hoge.com", true).toString ==
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;" data-toggle="tooltip" title="user" alt="@user" />"""
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;"
| alt="@user"
| data-toggle="tooltip" title="user" />""".stripMargin
)
}
it("should escape user name") {
implicit val context = Context(createSystemSettings(false), None, request)
val provider = new AvatarImageProviderImpl(None)
assert(
provider.toHtml("""<user>"<name>""", 20, "hoge@hoge.com").toString ==
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;"
| alt="@&lt;user&gt;&quot;&lt;name&gt;" />""".stripMargin
)
assert(
provider.toHtml("""<user>"<name>""", 20, "hoge@hoge.com", true).toString ==
"""<img src="/_unknown/_avatar" class="avatar-mini" style="width: 20px; height: 20px;"
| alt="@&lt;user&gt;&quot;&lt;name&gt;"
| data-toggle="tooltip" title="&lt;user&gt;&quot;&lt;name&gt;" />""".stripMargin
)
}
}
@@ -140,8 +166,8 @@ class AvatarImageProviderSpec extends AnyFunSpec {
limitVisibleRepositories = false,
ssh = Ssh(
enabled = false,
sshHost = None,
sshPort = None
bindAddress = None,
publicAddress = None
),
useSMTP = false,
smtp = None,

View File

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