Compare commits

..

55 Commits

Author SHA1 Message Date
Naoki Takezoe
f16cc117a9 Release 4.44.0 (#3842) 2025-09-23 09:02:24 +09:00
Naoki Takezoe
af66f8f746 Fix Repository Contents Upload API for nested path (#3843) 2025-09-23 09:01:30 +09:00
Scala Steward
d9cc57e8e0 Update oauth2-oidc-sdk to 11.29.1 2025-09-23 06:51:06 +09:00
Scala Steward
72b6dad3a2 Update mockito-core to 5.20.0 2025-09-21 06:10:09 +09:00
Scala Steward
18f396b4a2 Update postgresql to 42.7.8 2025-09-19 13:46:36 +09:00
Scala Steward
9f3fde8de2 Update tika-core to 3.2.3 2025-09-17 07:14:08 +09:00
scala-steward-bot
dfd6f80b63 Update scalafmt-core to 3.9.10 (#3838) 2025-09-16 22:15:57 +09:00
Scala Steward
119d91210c Update typesafe:config to 1.4.5 2025-09-11 17:29:06 +09:00
Scala Steward
75ef30ee03 Update sbt-license-report to 1.8.0 2025-09-11 06:32:21 +09:00
Scala Steward
d1cf9dd600 Update scala3-library to 3.7.3 2025-09-09 06:18:29 +09:00
Scala Steward
9c9fea908c Update sbt, sbt-dependency-tree, ... to 1.11.6 2025-09-07 08:51:05 +09:00
kenji yoshida
1145c4d0f6 update build.sbt. prepare sbt 2 2025-09-05 20:03:01 +09:00
Scala Steward
d847fc6e0f Update github-api to 1.330 2025-09-05 05:36:45 +09:00
Scala Steward
bb9585f7a6 Update oauth2-oidc-sdk to 11.28 2025-08-31 06:58:03 +09:00
Scala Steward
e91411fa45 Update sbt, sbt-dependency-tree, ... to 1.11.5 2025-08-26 08:30:43 +09:00
dependabot[bot]
adbc065a6f Bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 17:59:37 +09:00
Scala Steward
862283b729 Update apache-sshd to 2.16.0 2025-08-24 06:44:33 +09:00
Naoki Takezoe
217df7012c Fix Wiki branch resolution in blob endpoint (#3826) 2025-08-23 17:11:11 +09:00
Naoki Takezoe
e672d41e77 Fix downloading branch that contains slash (#3825) 2025-08-23 16:49:18 +09:00
Scala Steward
d975700bd4 Update HikariCP to 7.0.2 2025-08-20 03:24:57 +09:00
dependabot[bot]
f65e41561a Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 17:51:52 +09:00
Scala Steward
dab4f33ed9 Update oauth2-oidc-sdk to 11.27.1 2025-08-16 12:30:05 +09:00
Scala Steward
5ff45ef5ae Update jetty-http, jetty-io, jetty-runner, ... to 10.0.26 2025-08-16 06:35:41 +09:00
Scala Steward
af7c622647 Update mockito-core to 5.19.0 2025-08-15 18:58:23 +09:00
Scala Steward
f7027e57df Update HikariCP to 7.0.1 2025-08-09 12:01:30 +09:00
Scala Steward
6a4719469d Update tika-core to 3.2.2 2025-08-08 19:03:31 +09:00
Scala Steward
6ebc865ba5 Update sbt, sbt-dependency-tree, ... to 1.11.4 2025-08-05 07:04:59 +09:00
Naoki Takezoe
0c1e8b932b Reject direct push to branch if branch protection is enabled (#3791) 2025-08-03 11:58:28 +09:00
scala-steward-bot
fda67a32e2 Update scalafmt-core to 3.9.9 (#3815) 2025-08-03 01:31:06 +09:00
Scala Steward
14d7e9ee90 Update commons-net to 3.12.0 2025-08-02 06:56:11 +09:00
Naoki Takezoe
b7b7322cce Fix branch selector in repository viewer (#3813) 2025-08-01 02:33:05 +09:00
Scala Steward
5eb44398d0 Update oauth2-oidc-sdk to 11.27 2025-07-30 10:09:17 +09:00
Scala Steward
be7bb255c3 Update commons-compress to 1.28.0 2025-07-30 09:54:03 +09:00
Scala Steward
cb9522d416 Update github-api to 1.329 2025-07-30 06:56:36 +09:00
Scala Steward
d80afb473b Update scala3-library to 3.7.2 2025-07-30 06:49:49 +09:00
Scala Steward
607d85c661 Update HikariCP to 7.0.0 2025-07-29 05:59:20 +09:00
Scala Steward
a5fab3bc96 Update oauth2-oidc-sdk to 11.26.1 2025-07-25 18:55:51 +09:00
Scala Steward
eb403ada58 Update HikariCP to 6.3.2 2025-07-24 17:34:00 +09:00
Naoki Takezoe
911c102f39 Improve logging in initialization process (#3804) 2025-07-21 12:10:56 +09:00
Naoki Takezoe
bf23e854f8 Exclude sshd-spring-sftp (#3803) 2025-07-21 11:58:22 +09:00
Scala Steward
52427c0a1e Update HikariCP to 6.3.1 2025-07-21 06:15:54 +09:00
Naoki Takezoe
d8e5ac585c Disable blank issue (#3801) 2025-07-20 10:52:14 +09:00
Naoki Takezoe
2fbeef73b0 Move issue_template.yml to .github directory (#3800) 2025-07-20 10:42:25 +09:00
Naoki Takezoe
15e39572dd Update issue template (#3799) 2025-07-20 10:33:40 +09:00
Scala Steward
9eac4f42c5 Update commons-io to 2.20.0 2025-07-20 06:04:11 +09:00
scala-steward-bot
01d18bb5c3 Update typesafe:config to 1.4.4 (#3796) 2025-07-13 08:52:18 +09:00
scala-steward-bot
00258e9125 Update tika-core to 3.2.1 (#3795) 2025-07-13 08:52:02 +09:00
Scala Steward
046b337337 Update java-diff-utils to 4.16 2025-07-08 17:02:18 +09:00
Naoki Takezoe
46cc7b6fd3 Update H2 database migration guide (#3793) 2025-07-06 21:24:28 +09:00
Scala Steward
59344b4f05 Update sbt, sbt-dependency-tree, ... to 1.11.3 2025-07-06 07:11:59 +09:00
Naoki Takezoe
c4d8af02b2 Fix wrong redirect after sign-in when user-defined CSS is used (#3789) 2025-07-05 12:08:20 +09:00
Scala Steward
a10bc3687a Update sbt-twirl, twirl-api to 2.0.9 2025-07-01 06:36:38 +09:00
Scala Steward
b0d21dee42 Update sbt-scalafmt to 2.5.5 2025-06-30 17:35:49 +09:00
Naoki Takezoe
13ea0e7507 Update release procedure doc (#3785) 2025-06-29 13:57:16 +09:00
takezoe
d66fdaede5 Update README and CHANGELOG 2025-06-29 13:33:29 +09:00
41 changed files with 874 additions and 456 deletions

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -1,7 +1,5 @@
name: Bug report
description: Report a problem with GitBucket
title: "[Bug] "
labels: [bug]
name: Report issue
description: Report a problem or feature request with GitBucket
body:
- type: markdown
attributes:
@@ -10,6 +8,7 @@ body:
- Read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- Searched for similar existing issues
- Read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
- You can use [Gitter chat room](https://gitter.im/gitbucket/gitbucket) instead of GitHub Issues for casual discussion or inquiry
- type: checkboxes
id: prerequisites

View File

@@ -11,7 +11,7 @@ jobs:
matrix:
java: [17, 21]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Cache
uses: actions/cache@v4
env:
@@ -23,7 +23,7 @@ jobs:
~/.cache/coursier/v1
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: ${{ matrix.java }}
distribution: adopt

View File

@@ -1,4 +1,4 @@
version = "3.9.8"
version = "3.9.10"
project.git = true
maxColumn = 120

View File

@@ -1,13 +1,17 @@
# Changelog
All changes to the project will be documented in this file.
## 4.44.0 - 23 Sep 2025
- Enhanced branch protection which supports rejecting users fo push, etc.
- Improve logging for initialization errors
## 4.43.0 - 29 Jun 2025
- Upgrade H2 database from 1.x to 2.x
Note that upgrading from h2 1.x to 2.x requires data file migration: https://www.h2database.com/html/migration-to-v2.html
It can't be done automatically using GitBucket's auto migration mechanism because it relies on database itself. So, users who use h2 will have to dump and recreate their database manually with the following steps:
```
```bash
# Export database using the current version of H2
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar
$ java -cp h2-1.4.199.jar org.h2.tools.Script -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
@@ -17,6 +21,14 @@ $ curl -O https://repo1.maven.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.ja
$ java -cp h2-2.3.232.jar org.h2.tools.RunScript -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
```
In addition, if `~/.gitbucket/database.conf` has the following configuration, remove `;MVCC=true` from `url`.
```
db {
url = "jdbc:h2:${DatabaseHome};MVCC=true" // => "jdbc:h2:${DatabaseHome}"
...
}
```
## 4.42.1 - 20 Jan 2025
- Fix LDAP issue with SSL

View File

@@ -56,18 +56,19 @@ Support
--------
- If you have any questions about GitBucket, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- If you can't find same question and report, send it to our [Gitter chat 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.43.x
What's New in 4.44.x
-------------
## 4.43.0 - 29 Jun 2025
- Upgrade H2 database from 1.x to 2.x
## 4.44.0 - 23 Sep 2025
- Enhanced branch protection which supports rejecting users fo push, etc.
- Improve logging for initialization errors
Note that upgrading from h2 1.x to 2.x requires data file migration: https://www.h2database.com/html/migration-to-v2.html
Note that you have to migrate h2 database file if you will upgrade GitBucket from 4.42 or before to 4.43 or later and you are using the default h2 database because h2 1.x and h2.x don't have compatibility: https://www.h2database.com/html/migration-to-v2.html
It can't be done automatically using GitBucket's auto migration mechanism because it relies on database itself. So, users who use h2 will have to dump and recreate their database manually with the following steps:
```
```bash
# Export database using the current version of H2
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar
$ java -cp h2-1.4.199.jar org.h2.tools.Script -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
@@ -77,4 +78,12 @@ $ curl -O https://repo1.maven.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.ja
$ java -cp h2-2.3.232.jar org.h2.tools.RunScript -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
```
In addition, if `~/.gitbucket/database.conf` has the following configuration, remove `;MVCC=true` from `url`.
```
db {
url = "jdbc:h2:${DatabaseHome};MVCC=true" // => "jdbc:h2:${DatabaseHome}"
...
}
```
See the [change log](CHANGELOG.md) for all the past updates.

View File

@@ -2,9 +2,9 @@ import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.43.0"
val GitBucketVersion = "4.44.0"
val ScalatraVersion = "3.1.2"
val JettyVersion = "10.0.25"
val JettyVersion = "10.0.26"
val JgitVersion = "6.10.1.202505221210-r"
lazy val root = (project in file("."))
@@ -16,7 +16,7 @@ name := Name
version := GitBucketVersion
scalaVersion := "2.13.16"
crossScalaVersions += "3.7.1"
crossScalaVersions += "3.7.3"
// scalafmtOnCompile := true
@@ -29,45 +29,46 @@ libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra-json-javax" % ScalatraVersion,
"org.scalatra" %% "scalatra-forms-javax" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "4.1.0-M8",
"commons-io" % "commons-io" % "2.19.0",
"commons-io" % "commons-io" % "2.20.0",
"io.github.gitbucket" % "solidbase" % "1.1.0",
"io.github.gitbucket" % "markedj" % "1.0.20",
"org.tukaani" % "xz" % "1.10",
"org.apache.commons" % "commons-compress" % "1.27.1",
"org.apache.commons" % "commons-compress" % "1.28.0",
"org.apache.commons" % "commons-email" % "1.6.0",
"commons-net" % "commons-net" % "3.11.1",
"commons-net" % "commons-net" % "3.12.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.14",
"org.apache.sshd" % "apache-sshd" % "2.15.0" exclude ("org.slf4j", "slf4j-jdk14") exclude (
"org.apache.sshd" % "apache-sshd" % "2.16.0" exclude ("org.slf4j", "slf4j-jdk14") exclude (
"org.apache.sshd",
"sshd-mina"
) exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "3.2.0",
) exclude ("org.apache.sshd", "sshd-netty")
exclude ("org.apache.sshd", "sshd-spring-sftp"),
"org.apache.tika" % "tika-core" % "3.2.3",
"com.github.takezoe" %% "blocking-slick" % "0.0.14",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "2.3.232",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.12",
"org.postgresql" % "postgresql" % "42.7.7",
"org.postgresql" % "postgresql" % "42.7.8",
"ch.qos.logback" % "logback-classic" % "1.5.18",
"com.zaxxer" % "HikariCP" % "6.3.0" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.3",
"com.zaxxer" % "HikariCP" % "7.0.2" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.5",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.15",
"io.github.java-diff-utils" % "java-diff-utils" % "4.16",
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.20",
"com.github.zafarkhaja" % "java-semver" % "0.10.2",
"com.nimbusds" % "oauth2-oidc-sdk" % "11.26",
"com.nimbusds" % "oauth2-oidc-sdk" % "11.29.1",
"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-javax" % ScalatraVersion % "test",
"org.mockito" % "mockito-core" % "5.18.0" % "test",
"org.mockito" % "mockito-core" % "5.20.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.43.0" % "test",
"org.testcontainers" % "mysql" % "1.21.3" % "test",
"org.testcontainers" % "postgresql" % "1.21.3" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "1.1.1",
"org.kohsuke" % "github-api" % "1.327" % "test"
"org.kohsuke" % "github-api" % "1.330" % "test"
)
// Compiler settings
@@ -191,7 +192,7 @@ executableKey := {
// zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF")
val contentMappings = (temp.allPaths --- PathFinder(temp)).get pair { file =>
val contentMappings = (temp.allPaths --- PathFinder(temp)).get() pair { file =>
IO.relativizeFile(temp, file)
}
val manifest = new JarManifest

View File

@@ -38,15 +38,26 @@ Generate release files
For plug-in development, we have to publish the GitBucket jar file to the Maven central repository before release GitBucket itself.
First, hit following command to publish artifacts to the sonatype OSS repository:
First, start the sbt shell:
```bash
$ sbt publishSigned
```
Then logged-in to https://oss.sonatype.org/, close and release the repository.
Next, upload artifacts to Sonatype's Central Portal with the following command:
You need to wait up to a day until [gitbucket-notification-plugin](https://plugins.gitbucket-community.org/) which is default bundled plugin is built for new version of GitBucket.
```bash
$ sbt sonaUpload
```
Then logged-in to https://central.sonatype.com/ and publish the deployment.
You need to wait up to a day until default bundled plugins:
- https://github.com/gitbucket/gitbucket-notifications-plugin
- https://github.com/gitbucket/gitbucket-gist-plugin
- https://github.com/gitbucket/gitbucket-pages-plugin
- https://github.com/gitbucket/gitbucket-emoji-plugin
### Make release war file
@@ -55,5 +66,4 @@ Run `sbt executable`. The release war file and fingerprint are generated into `t
```bash
$ sbt executable
```
Create new release from the corresponded tag on GitHub, then upload generated jar file and fingerprints to the release.

View File

@@ -1 +1 @@
sbt.version=1.11.2
sbt.version=1.11.6

View File

@@ -1,11 +1,11 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4")
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.8")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5")
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.9")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.7.0")
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.8.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1")
addDependencyTreePlugin

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<!--================================================================================================-->
<!-- PROTECTED_BRANCH -->
<!--================================================================================================-->
<addColumn tableName="PROTECTED_BRANCH">
<column name="REQUIRED_STATUS_CHECK" type="boolean" nullable="false" defaultValue="false"/>
<column name="RESTRICTIONS" type="boolean" nullable="false" defaultValue="false"/>
</addColumn>
<sql>
UPDATE PROTECTED_BRANCH SET REQUIRED_STATUS_CHECK = TRUE
WHERE EXISTS (SELECT * FROM PROTECTED_BRANCH_REQUIRE_CONTEXT
WHERE PROTECTED_BRANCH.USER_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.USER_NAME
AND PROTECTED_BRANCH.REPOSITORY_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.REPOSITORY_NAME
AND PROTECTED_BRANCH.BRANCH = PROTECTED_BRANCH_REQUIRE_CONTEXT.BRANCH)
</sql>
<!--================================================================================================-->
<!-- PROTECTED_BRANCH_RESTRICTIONS_USER -->
<!--================================================================================================-->
<createTable tableName="PROTECTED_BRANCH_RESTRICTION">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="BRANCH" type="varchar(100)" nullable="false"/>
<column name="ALLOWED_USER" type="varchar(255)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_PK" tableName="PROTECTED_BRANCH_RESTRICTION" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, ALLOWED_USER"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK0" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK1" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="ALLOWED_USER" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
</changeSet>

View File

@@ -120,7 +120,8 @@ object GitBucketCoreModule
new Version("4.41.0"),
new Version("4.42.0", new LiquibaseMigration("update/gitbucket-core_4.42.xml")),
new Version("4.42.1"),
new Version("4.43.0")
new Version("4.43.0"),
new Version("4.44.0", new LiquibaseMigration("update/gitbucket-core_4.44.xml"))
) {
java.util.logging.Logger.getLogger("liquibase").setLevel(Level.SEVERE)
}

View File

@@ -6,7 +6,7 @@ import gitbucket.core.util.RepositoryName
* https://developer.github.com/v3/repos/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtection)(
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtectionResponse)(
repositoryName: RepositoryName
) extends FieldSerializable {
val _links =

View File

@@ -0,0 +1,21 @@
package gitbucket.core.api
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtectionRequest(
enabled: Boolean,
required_status_checks: Option[ApiBranchProtectionRequest.Status],
restrictions: Option[ApiBranchProtectionRequest.Restrictions],
enforce_admins: Option[Boolean]
)
object ApiBranchProtectionRequest {
/** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtectionRequest)
case class Status(
contexts: Seq[String]
)
case class Restrictions(users: Seq[String])
}

View File

@@ -4,55 +4,68 @@ import gitbucket.core.service.ProtectedBranchService
import org.json4s._
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtection(
case class ApiBranchProtectionResponse(
url: Option[ApiPath], // for output
enabled: Boolean,
required_status_checks: Option[ApiBranchProtection.Status]
required_status_checks: Option[ApiBranchProtectionResponse.Status],
restrictions: Option[ApiBranchProtectionResponse.Restrictions],
enforce_admins: Option[ApiBranchProtectionResponse.EnforceAdmins]
) {
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
def status: ApiBranchProtectionResponse.Status =
required_status_checks.getOrElse(ApiBranchProtectionResponse.statusNone)
}
object ApiBranchProtection {
object ApiBranchProtectionResponse {
/** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtection)
case class EnforceAdmins(enabled: Boolean)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection =
ApiBranchProtection(
// /** form for enabling-and-disabling-branch-protection */
// case class EnablingAndDisabling(protection: ApiBranchProtectionResponse)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtectionResponse =
ApiBranchProtectionResponse(
url = Some(
ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection"
)
),
enabled = info.enabled,
required_status_checks = Some(
required_status_checks = info.contexts.map { contexts =>
Status(
Some(
ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks"
)
),
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators),
info.contexts,
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.enforceAdmins),
contexts,
Some(
ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks/contexts"
)
)
)
)
},
restrictions = info.restrictionsUsers.map { restrictionsUsers =>
Restrictions(restrictionsUsers)
},
enforce_admins = if (info.enabled) Some(EnforceAdmins(info.enforceAdmins)) else None
)
val statusNone = Status(None, Off, Seq.empty, None)
val statusNone: Status = Status(None, Off, Seq.empty, None)
case class Status(
url: Option[ApiPath], // for output
enforcement_level: EnforcementLevel,
contexts: Seq[String],
contexts_url: Option[ApiPath] // for output
)
sealed class EnforcementLevel(val name: String)
case object Off extends EnforcementLevel("off")
case object NonAdmins extends EnforcementLevel("non_admins")
case object Everyone extends EnforcementLevel("everyone")
object EnforcementLevel {
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel =
if (enabled) {
@@ -66,6 +79,8 @@ object ApiBranchProtection {
}
}
case class Restrictions(users: Seq[String])
implicit val enforcementLevelSerializer: CustomSerializer[EnforcementLevel] =
new CustomSerializer[EnforcementLevel](format =>
(

View File

@@ -44,7 +44,7 @@ object JsonFormat {
FieldSerializer[ApiCommits.File]() +
FieldSerializer[ApiRelease]() +
FieldSerializer[ApiReleaseAsset]() +
ApiBranchProtection.enforcementLevelSerializer
ApiBranchProtectionResponse.enforcementLevelSerializer
def apiPathSerializer(c: Context) =
new CustomSerializer[ApiPath](_ =>

View File

@@ -261,11 +261,22 @@ trait IndexControllerBase extends ControllerBase {
/**
* JSON API for checking user or group existence.
*
* Returns a single string which is any of "group", "user" or "".
* Additionally, check whether the user is writable to the repository
* if "owner" and "repository" are given,
*/
post("/_user/existence")(usersOnly {
getAccountByUserNameIgnoreCase(params("userName")).map { account =>
if (account.isGroupAccount) "group" else "user"
if (!account.isGroupAccount && params.get("repository").isDefined && params.get("owner").isDefined) {
getRepository(params("owner"), params("repository"))
.collect {
case repository if isWritable(repository.repository, Some(account)) => "user"
}
.getOrElse("")
} else {
if (account.isGroupAccount) "group" else "user"
}
} getOrElse ""
})

View File

@@ -33,7 +33,7 @@ trait PreProcessControllerBase extends ControllerBase {
if (
!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
!context.currentPath.startsWith("/plugin-assets") &&
!context.currentPath.startsWith("/plugin-assets") && !context.currentPath.equals("/user.css") &&
!PluginRegistry().getAnonymousAccessiblePaths().exists { path =>
context.currentPath.startsWith(path)
}

View File

@@ -203,7 +203,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
if (!repository.branchList.contains(branch)) {
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val protection = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatusContexts(
repository.owner,
repository.name,

View File

@@ -1076,14 +1076,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
redirect(s"${repository.owner}/${repository.name}/releases")
})
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
val name = params("name")
archiveRepository(name, repository, "")
})
get("/:owner/:repository/archive/*/:name")(referrersOnly { repository =>
val name = params("name")
val path = multiParams("splat").head
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
val name = multiParams("splat").mkString("/")
val path = params.get("path").getOrElse("")
archiveRepository(name, repository, path)
})

View File

@@ -306,7 +306,8 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
JGitUtil.getCommitLog(git, "master") match {
val branch = getWikiBranch(repository.owner, repository.name)
JGitUtil.getCommitLog(git, branch) match {
case Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository))
case Left(_) => NotFound()
}
@@ -316,7 +317,8 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
val path = multiParams("splat").head
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve("master"))
val branch = getWikiBranch(repository.owner, repository.name)
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
responseRawFile(git, objectId, path, repository)

View File

@@ -5,7 +5,7 @@ import gitbucket.core.service.{AccountService, ProtectedBranchService, Repositor
import gitbucket.core.util.*
import gitbucket.core.util.Directory.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.JGitUtil.getBranchesNoMergeInfo
import gitbucket.core.util.JGitUtil.{getBranchesNoMergeInfo, processTree}
import org.eclipse.jgit.api.Git
import org.scalatra.NoContent
@@ -43,7 +43,9 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
} yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat(
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtection(protection))(RepositoryName(repository))
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtectionResponse(protection))(
RepositoryName(repository)
)
)
}) getOrElse NotFound()
}
@@ -58,7 +60,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat(
ApiBranchProtection(protection)
ApiBranchProtectionResponse(protection)
)
} else { NotFound() }
})
@@ -138,7 +140,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat(
ApiBranchProtection(protection).required_status_checks
ApiBranchProtectionResponse(protection).required_status_checks
)
} else { NotFound() }
})
@@ -262,7 +264,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
(for {
branch <- params.get("splat") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
protection <- extractFromJsonBody[ApiBranchProtectionRequest.EnablingAndDisabling].map(_.protection)
br <- getBranchesNoMergeInfo(git).find(_.name == branch)
} yield {
if (protection.enabled) {
@@ -270,13 +272,17 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
repository.owner,
repository.name,
branch,
protection.status.enforcement_level == ApiBranchProtection.Everyone,
protection.status.contexts
protection.enforce_admins.getOrElse(false),
protection.required_status_checks.isDefined,
protection.required_status_checks.map(_.contexts).getOrElse(Nil),
protection.restrictions.isDefined,
protection.restrictions.map(_.users).getOrElse(Nil)
)
} else {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), protection)(RepositoryName(repository)))
val response = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), response)(RepositoryName(repository)))
}) getOrElse NotFound()
}
})

View File

@@ -142,10 +142,11 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
revCommit.name
}
val paths = multiParams("splat").head.split("/")
val fullPath = multiParams("splat").head
val paths = fullPath.split("/")
val path = paths.take(paths.size - 1).toList.mkString("/")
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val fileInfo = getFileInfo(git, commit, path, false)
val fileInfo = getFileInfo(git, commit, fullPath, ignoreCase = false)
fileInfo match {
case Some(f) if !data.sha.contains(f.id.getName) =>

View File

@@ -1,16 +1,18 @@
package gitbucket.core.model
trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
import profile.api.*
lazy val ProtectedBranches = TableQuery[ProtectedBranches]
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN")
def * = (userName, repositoryName, branch, statusCheckAdmin).mapTo[ProtectedBranch]
def byPrimaryKey(userName: String, repositoryName: String, branch: String) =
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") // enforceAdmins
val requiredStatusCheck = column[Boolean]("REQUIRED_STATUS_CHECK")
val restrictions = column[Boolean]("RESTRICTIONS")
def * =
(userName, repositoryName, branch, statusCheckAdmin, requiredStatusCheck, restrictions).mapTo[ProtectedBranch]
def byPrimaryKey(userName: String, repositoryName: String, branch: String): Rep[Boolean] =
byBranch(userName, repositoryName, branch)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) =
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]): Rep[Boolean] =
byBranch(userName, repositoryName, branch)
}
@@ -22,8 +24,27 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
def * =
(userName, repositoryName, branch, context).mapTo[ProtectedBranchContext]
}
lazy val ProtectedBranchRestrictions = TableQuery[ProtectedBranchRestrictions]
class ProtectedBranchRestrictions(tag: Tag)
extends Table[ProtectedBranchRestriction](tag, "PROTECTED_BRANCH_RESTRICTION")
with BranchTemplate {
val allowedUser = column[String]("ALLOWED_USER")
def * = (userName, repositoryName, branch, allowedUser).mapTo[ProtectedBranchRestriction]
def byPrimaryKey(userName: String, repositoryName: String, branch: String, allowedUser: String): Rep[Boolean] =
this.userName === userName.bind && this.repositoryName === repositoryName.bind && this.branch === branch.bind && this.allowedUser === allowedUser.bind
}
}
case class ProtectedBranch(userName: String, repositoryName: String, branch: String, statusCheckAdmin: Boolean)
case class ProtectedBranch(
userName: String,
repositoryName: String,
branch: String,
enforceAdmins: Boolean,
requiredStatusCheck: Boolean,
restrictions: Boolean
)
case class ProtectedBranchContext(userName: String, repositoryName: String, branch: String, context: String)
case class ProtectedBranchRestriction(userName: String, repositoryName: String, branch: String, allowedUser: String)

View File

@@ -1,9 +1,10 @@
package gitbucket.core.service
import gitbucket.core.model.{Session => _, _}
import gitbucket.core.plugin.ReceiveHook
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.*
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.{CommitState, ProtectedBranch, ProtectedBranchContext, ProtectedBranchRestriction, Role}
import gitbucket.core.util.SyntaxSugars.*
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
trait ProtectedBranchService {
@@ -13,17 +14,27 @@ trait ProtectedBranchService {
): Option[ProtectedBranchInfo] =
ProtectedBranches
.joinLeft(ProtectedBranchContexts)
.on { case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) }
.map { case (pb, c) => pb -> c.map(_.context) }
.on { case pb ~ c => pb.byBranch(c.userName, c.repositoryName, c.branch) }
.joinLeft(ProtectedBranchRestrictions)
.on { case pb ~ c ~ r => pb.byBranch(r.userName, r.repositoryName, r.branch) }
.map { case pb ~ c ~ r => pb -> (c.map(_.context), r.map(_.allowedUser)) }
.filter(_._1.byPrimaryKey(owner, repository, branch))
.list
.groupBy(_._1)
.headOption
.map { p =>
p._1 -> p._2.flatMap(_._2)
.map { (p: (ProtectedBranch, List[(ProtectedBranch, (Option[String], Option[String]))])) =>
p._1 -> (p._2.flatMap(_._2._1), p._2.flatMap(_._2._2))
}
.map { case (t1, contexts) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, t1.branch, true, contexts, t1.statusCheckAdmin)
.map { case (t1, (contexts, users)) =>
new ProtectedBranchInfo(
t1.userName,
t1.repositoryName,
t1.branch,
true,
if (t1.requiredStatusCheck) Some(contexts) else None,
t1.enforceAdmins,
if (t1.restrictions) Some(users) else None
)
}
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit
@@ -40,19 +51,32 @@ trait ProtectedBranchService {
owner: String,
repository: String,
branch: String,
includeAdministrators: Boolean,
contexts: Seq[String]
enforceAdmins: Boolean,
requiredStatusCheck: Boolean,
contexts: Seq[String],
restrictions: Boolean,
restrictionsUsers: Seq[String]
)(implicit session: Session): Unit = {
disableBranchProtection(owner, repository, branch)
ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty))
contexts.map { context =>
ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context))
ProtectedBranches.insert(
ProtectedBranch(owner, repository, branch, enforceAdmins, requiredStatusCheck, restrictions)
)
if (restrictions) {
restrictionsUsers.foreach { user =>
ProtectedBranchRestrictions.insert(ProtectedBranchRestriction(owner, repository, branch, user))
}
}
if (requiredStatusCheck) {
contexts.foreach { context =>
ProtectedBranchContexts.insert(ProtectedBranchContext(owner, repository, branch, context))
}
}
}
def disableBranchProtection(owner: String, repository: String, branch: String)(implicit session: Session): Unit =
ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete
}
object ProtectedBranchService {
@@ -101,6 +125,7 @@ object ProtectedBranchService {
)
}
} else {
println("-> else")
None
}
}
@@ -117,12 +142,16 @@ object ProtectedBranchService {
* When enabled, commits must first be pushed to another branch,
* then merged or pushed directly to test after status checks have passed.
*/
contexts: Seq[String],
contexts: Option[Seq[String]],
/**
* Include administrators
* Enforce required status checks for repository administrators.
*/
includeAdministrators: Boolean
enforceAdmins: Boolean,
/**
* Users who can push to the branch.
*/
restrictionsUsers: Option[Seq[String]]
) extends AccountService
with RepositoryService
with CommitStatusService {
@@ -148,42 +177,66 @@ object ProtectedBranchService {
session: Session
): Option[String] = {
if (enabled) {
command.getType() match {
command.getType match {
case ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
Some("Cannot force-push to a protected branch")
case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if !isPushAllowed(pusher) =>
Some("You do not have permission to push to this branch")
case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
unSuccessedContexts(command.getNewId.name) match {
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 s if s.sizeIs == 1 => Some(s"""Required status check "${s.head}" is expected""")
case s if s.sizeIs >= 1 =>
Some(s"${s.size} of ${contexts.map(_.size).getOrElse(0)} required status checks are expected")
case _ => None
}
case ReceiveCommand.Type.DELETE =>
Some("Cannot delete a protected branch")
Some("You do not have permission to push to this branch")
case _ => None
}
} else {
None
}
}
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] =
if (contexts.isEmpty) {
Set.empty
} else {
contexts.toSet -- getCommitStatuses(owner, repository, sha1)
.filter(_.state == CommitState.SUCCESS)
.map(_.context)
.toSet
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = {
contexts match {
case None => Set.empty
case Some(x) if x.isEmpty => Set.empty
case Some(x) =>
x.toSet -- getCommitStatuses(owner, repository, sha1)
.filter(_.state == CommitState.SUCCESS)
.map(_.context)
.toSet
}
}
def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match {
case _ if !enabled => false
case _ if contexts.isEmpty => false
case _ if includeAdministrators => true
case p if isAdministrator(p) => false
case _ => true
case _ if !enabled => false
case _ if contexts.isEmpty => false
case _ if enforceAdmins => true
case p if isAdministrator(p) => false
case _ => true
}
def isPushAllowed(pusher: String)(implicit session: Session): Boolean = pusher match {
case _ if !enabled || restrictionsUsers.isEmpty => true
case _ if restrictionsUsers.get.contains(pusher) => true
case p if isAdministrator(p) && enforceAdmins => false
case _ => false
}
}
object ProtectedBranchInfo {
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo =
ProtectedBranchInfo(owner, repository, branch, false, Nil, false)
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo = {
ProtectedBranchInfo(
owner,
repository,
branch,
enabled = false,
contexts = None,
enforceAdmins = false,
restrictionsUsers = None
)
}
}
}

View File

@@ -653,18 +653,18 @@ object PullRequestService {
commitIdTo: String
) {
val hasConflict = conflictMessage.isDefined
val hasConflict: Boolean = conflictMessage.isDefined
val statuses: List[CommitStatus] =
commitStatuses ++ (branchProtection.contexts.toSet -- commitStatuses.map(_.context).toSet)
commitStatuses ++ (branchProtection.contexts.getOrElse(Nil).toSet -- commitStatuses.map(_.context).toSet)
.map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context =>
statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS)
)
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(
val hasRequiredStatusProblem: Boolean = needStatusCheck && branchProtection.contexts
.getOrElse(Nil)
.exists(context => !statuses.find(_.context == context).map(_.state).contains(CommitState.SUCCESS))
val hasProblem: Boolean = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(
statuses.map(_.state).toSet
) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
val canUpdate: Boolean = branchIsOutOfDate && !hasConflict
val canMerge: Boolean = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary: (CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)
@@ -672,8 +672,8 @@ object PullRequestService {
state -> summary
}
lazy val statusesAndRequired: List[(CommitStatus, Boolean)] = statuses.map { s =>
s -> branchProtection.contexts.contains(s.context)
s -> branchProtection.contexts.getOrElse(Nil).contains(s.context)
}
lazy val isAllSuccess = commitStateSummary._1 == CommitState.SUCCESS
lazy val isAllSuccess: Boolean = commitStateSummary._1 == CommitState.SUCCESS
}
}

View File

@@ -1,7 +1,7 @@
package gitbucket.core.service
import gitbucket.core.api.JsonFormat
import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{CloseIssueInfo, PushInfo}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.SystemSettings
@@ -11,14 +11,14 @@ import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.{JGitUtil, LockUtil}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.lib._
import org.eclipse.jgit.lib.*
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import scala.util.Using
trait RepositoryCommitFileService {
self: AccountService & ActivityService & IssuesService & PullRequestService & WebHookPullRequestService &
RepositoryService =>
RepositoryService & ProtectedBranchService =>
/**
* Create multiple files by callback function.
@@ -92,10 +92,10 @@ trait RepositoryCommitFileService {
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
val newPath = newFileName.map { newFileName =>
if (path.length == 0) newFileName else s"${path}/${newFileName}"
if (path.isEmpty) newFileName else s"${path}/${newFileName}"
}
val oldPath = oldFileName.map { oldFileName =>
if (path.length == 0) oldFileName else s"${path}/${oldFileName}"
if (path.isEmpty) oldFileName else s"${path}/${oldFileName}"
}
_createFiles(repository, branch, message, pusherAccount, committerName, committerMailAddress, settings) {
@@ -139,7 +139,6 @@ trait RepositoryCommitFileService {
)(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = {
LockUtil.lock(s"${repository.owner}/${repository.name}") {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val builder = DirCache.newInCore.builder()
@@ -168,7 +167,14 @@ trait RepositoryCommitFileService {
// call pre-commit hook
val error = PluginRegistry().getReceiveHooks.flatMap { hook =>
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, pusherAccount.userName, false)
hook.preReceive(
repository.owner,
repository.name,
receivePack,
receiveCommand,
pusherAccount.userName,
mergePullRequest = false
)
}.headOption
error match {
@@ -194,7 +200,8 @@ trait RepositoryCommitFileService {
// record activity
updateLastActivityDate(repository.owner, repository.name)
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
val pushInfo = PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo))
val pushInfo =
PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo))
recordActivity(pushInfo)
// create issue comment by commit message
@@ -221,7 +228,14 @@ trait RepositoryCommitFileService {
// call post-commit hook
PluginRegistry().getReceiveHooks.foreach { hook =>
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false)
hook.postReceive(
repository.owner,
repository.name,
receivePack,
receiveCommand,
committerName,
mergePullRequest = false
)
}
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))

View File

@@ -654,11 +654,11 @@ trait RepositoryService {
def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if (a.isAdmin) => true
case Some(a) if (a.userName == owner) => true
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true
case _ => false
case Some(a) if a.isAdmin => true
case Some(a) if a.userName == owner => true
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a) if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName) => true
case _ => false
}
}
@@ -666,11 +666,11 @@ trait RepositoryService {
s: Session
): Boolean = {
loginAccount match {
case Some(a) if (a.isAdmin) => true
case Some(a) if (a.userName == owner) => true
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if a.isAdmin => true
case Some(a) if a.userName == owner => true
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a)
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName)) =>
if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName) =>
true
case _ => false
}
@@ -678,12 +678,12 @@ trait RepositoryService {
def hasGuestRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if (a.isAdmin) => true
case Some(a) if (a.userName == owner) => true
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if a.isAdmin => true
case Some(a) if a.userName == owner => true
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a)
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST))
.contains(a.userName)) =>
if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST))
.contains(a.userName) =>
true
case _ => false
}
@@ -694,17 +694,29 @@ trait RepositoryService {
true
} else {
loginAccount match {
case Some(x) if (x.isAdmin) => true
case Some(x) if (repository.userName == x.userName) => true
case Some(x) if (getGroupMembers(repository.userName).exists(_.userName == x.userName)) => true
case Some(x)
if (getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName)) =>
case Some(x) if x.isAdmin => true
case Some(x) if repository.userName == x.userName => true
case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
case Some(x) if getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName) =>
true
case _ => false
}
}
}
def isWritable(repository: Repository, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(x) if x.isAdmin => true
case Some(x) if repository.userName == x.userName => true
case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
case Some(x)
if getCollaboratorUserNames(repository.userName, repository.repositoryName, Seq(Role.ADMIN, Role.DEVELOPER))
.contains(x.userName) =>
true
case _ => false
}
}
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
Query(Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)

View File

@@ -9,11 +9,11 @@ import gitbucket.core.api.JsonFormat.Context
import gitbucket.core.model.WebHook
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.WebHookService.*
import gitbucket.core.service.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{
BaseActivityInfo,
CloseIssueInfo,
@@ -33,9 +33,9 @@ import gitbucket.core.servlet.Database
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.http.server.GitServlet
import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport._
import org.eclipse.jgit.transport.resolver._
import org.eclipse.jgit.lib.*
import org.eclipse.jgit.transport.*
import org.eclipse.jgit.transport.resolver.*
import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
@@ -43,7 +43,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.internal.storage.file.FileRepository
import org.json4s.Formats
import org.json4s.convertToJsonInput
import org.json4s.jackson.Serialization._
import org.json4s.jackson.Serialization.*
/**
* Provides Git repository via HTTP.
@@ -117,7 +117,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
GitLfs.BatchResponseObject(
requestObject.oid,
requestObject.size,
true,
authenticated = true,
GitLfs.Actions(
upload = Some(
GitLfs.Action(
@@ -138,7 +138,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
GitLfs.BatchResponseObject(
requestObject.oid,
requestObject.size,
true,
authenticated = true,
GitLfs.Actions(
download = Some(
GitLfs.Action(
@@ -223,7 +223,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
}
}
import scala.jdk.CollectionConverters._
import scala.jdk.CollectionConverters.*
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook
@@ -242,6 +242,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
with WebHookPullRequestReviewCommentService
with CommitsService
with SystemSettingsService
with ProtectedBranchService
with RequestCache {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -253,7 +254,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
commands.asScala.foreach { command =>
// call pre-commit hook
PluginRegistry().getReceiveHooks
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, false))
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
.headOption
.foreach { error =>
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
@@ -428,8 +429,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
repositoryInfo,
newCommits,
ownerAccount,
newId = command.getNewId(),
oldId = command.getOldId()
newId = command.getNewId,
oldId = command.getOldId
)
}
}
@@ -453,7 +454,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// call post-commit hook
PluginRegistry().getReceiveHooks
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, false))
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
}
}
// update repository last modified time.

View File

@@ -6,9 +6,9 @@ import gitbucket.core.GitBucketCoreModule
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import gitbucket.core.util.JDBCUtil._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.util.Directory.*
import gitbucket.core.util.JDBCUtil.*
import gitbucket.core.model.Profile.profile.blockingApi.*
// Imported names have higher precedence than names, defined in other files.
// If Database is not bound by explicit import, then "Database" refers to the Database introduced by the wildcard import above.
import gitbucket.core.servlet.Database
@@ -20,7 +20,7 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
import org.apache.commons.io.{FileUtils, IOUtils}
import org.slf4j.LoggerFactory
import scala.jdk.CollectionConverters._
import scala.jdk.CollectionConverters.*
import scala.util.Using
/**
@@ -50,51 +50,57 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
// )
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if (dataDir != null) {
System.setProperty("gitbucket.home", dataDir)
}
org.h2.Driver.load()
try {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if (dataDir != null) {
System.setProperty("gitbucket.home", dataDir)
}
org.h2.Driver.load()
Database() withTransaction { session =>
val conn = session.conn
val manager = new JDBCVersionManager(conn)
Database() withTransaction { session =>
val conn = session.conn
val manager = new JDBCVersionManager(conn)
// Check version
checkVersion(manager, conn)
// Check version
checkVersion(manager, conn)
// Run normal migration
logger.info("Start schema update")
new Solidbase()
.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
// Run normal migration
logger.info("Start schema update")
new Solidbase()
.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
// Rescue code for users who updated from 3.14 to 4.0.0
// https://github.com/gitbucket/gitbucket/issues/1227
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
val databaseVersion = if (currentVersion == "4.0") {
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
"4.0.0"
} else currentVersion
// Rescue code for users who updated from 3.14 to 4.0.0
// https://github.com/gitbucket/gitbucket/issues/1227
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
val databaseVersion = if (currentVersion == "4.0") {
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
"4.0.0"
} else currentVersion
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
if (databaseVersion != gitbucketVersion) {
throw new IllegalStateException(
s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}."
)
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
if (databaseVersion != gitbucketVersion) {
throw new IllegalStateException(
s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}."
)
}
// Install bundled plugins
extractBundledPlugins()
// Load plugins
logger.info("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
}
// Install bundled plugins
extractBundledPlugins()
// Load plugins
logger.info("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
// // Start Quartz scheduler
// val scheduler = QuartzSchedulerExtension(system)
//
// scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
} catch {
case e: Exception =>
logger.error(e.getMessage, e)
throw e
}
// // Start Quartz scheduler
// val scheduler = QuartzSchedulerExtension(system)
//
// scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
}
private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
@@ -141,7 +147,7 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
Using.resource(cl.getResourceAsStream("bundle-plugins.txt")) { pluginsFile =>
if (pluginsFile != null) {
val plugins = IOUtils.readLines(pluginsFile, "UTF-8")
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
// val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
plugins.asScala.foreach { plugin =>
plugin.trim.split(":") match {

View File

@@ -15,9 +15,9 @@ trait OneselfAuthenticator { self: ControllerBase =>
private def authenticate(action: => Any) = {
context.loginAccount match {
case Some(x) if (x.isAdmin) => action
case Some(x) if (request.paths(0) == x.userName) => action
case _ => Unauthorized()
case Some(x) if x.isAdmin => action
case Some(x) if request.paths(0) == x.userName => action
case _ => Unauthorized()
}
}
}
@@ -26,7 +26,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
* Allows only the repository owner and administrators.
*/
trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def ownerOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = {
@@ -34,14 +34,14 @@ trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountSer
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
context.loginAccount match {
case Some(x) if (x.isAdmin) => action(repository)
case Some(x) if (repository.owner == x.userName) => action(repository)
case Some(x) if x.isAdmin => action(repository)
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 =>
case Some(x) if getGroupMembers(repository.owner).exists { m =>
m.userName == x.userName && m.isManager
}) =>
} =>
action(repository)
case Some(x) if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName)) =>
case Some(x) if getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName) =>
action(repository)
case _ => Unauthorized()
}
@@ -83,10 +83,10 @@ trait AdminAuthenticator { self: ControllerBase =>
* Allows only guests and signed in users who can access the repository.
*/
trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def referrersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = {
private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner")
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
@@ -103,12 +103,12 @@ trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & Account
* Allows only signed in users who have read permission for the repository.
*/
trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def readableUsersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
authenticate(action(form, _))
}
private def authenticate(action: (RepositoryInfo) => Any) = {
private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner")
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
@@ -125,24 +125,19 @@ trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & Ac
* Allows only signed in users who have write permission for the repository.
*/
trait WritableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def writableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def writableUsersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def writableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
authenticate(action(form, _))
}
private def authenticate(action: (RepositoryInfo) => Any) = {
private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner")
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
context.loginAccount match {
case Some(x) if (x.isAdmin) => action(repository)
case Some(x) if (userName == x.userName) => action(repository)
case Some(x) if (getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
case Some(x)
if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN, Role.DEVELOPER))
.contains(x.userName)) =>
action(repository)
case _ => Unauthorized()
if (isWritable(repository.repository, context.loginAccount)) {
action(repository)
} else {
Unauthorized()
}
} getOrElse NotFound()
}
@@ -159,9 +154,9 @@ trait GroupManagerAuthenticator { self: ControllerBase & AccountService =>
context.loginAccount match {
case Some(x) if x.isAdmin => action
case Some(x) if x.userName == request.paths(0) => action
case Some(x) if (getGroupMembers(request.paths(0)).exists { member =>
case Some(x) if getGroupMembers(request.paths(0)).exists { member =>
member.userName == x.userName && member.isManager
}) =>
} =>
action
case _ => Unauthorized()
}

View File

@@ -67,7 +67,7 @@
});
function updateBranchControlList(active) {
if (active == 'branches') {
if (active === 'branches') {
$('li#branch-control-tab-branches').addClass('active');
$('li#branch-control-tab-tags').removeClass('active');
@@ -81,7 +81,7 @@
} else {
$('#branch-control-input').attr('placeholder', 'Find branch ...');
}
} else if (active == 'tags') {
} else if (active === 'tags') {
$('li#branch-control-tab-branches').removeClass('active');
$('li#branch-control-tab-tags').addClass('active');

View File

@@ -40,7 +40,10 @@
<div id="branchCtrlWrapper" style="display:inline;">
@gitbucket.core.helper.html.branchcontrol(branch, repository, hasWritePermission){
@repository.branchList.map { x =>
<li><a href="@helpers.url(repository)/blob/@helpers.encodeRefName((x :: pathList).mkString("/"))">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
<li class="branch-control-item-branch"><a href="@helpers.url(repository)/blob/@helpers.encodeRefName((x :: pathList).mkString("/"))">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
}
@repository.tags.map { x =>
<li class="branch-control-item-tag"><a href="@helpers.url(repository)/blob/@helpers.encodeRefName((x.name :: pathList).mkString("/"))">@gitbucket.core.helper.html.checkicon(x.name == branch) @x.name</a></li>
}
}
</div>

View File

@@ -12,7 +12,10 @@
@if(pathList.isEmpty){
@gitbucket.core.helper.html.branchcontrol(branch, repository, hasWritePermission){
@repository.branchList.map { x =>
<li><a href="@helpers.url(repository)/commits/@helpers.encodeRefName(x)">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
<li class="branch-control-item-branch"><a href="@helpers.url(repository)/commits/@helpers.encodeRefName(x)">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
}
@repository.tags.map { x =>
<li class="branch-control-item-tag"><a href="@helpers.url(repository)/commits/@helpers.encodeRefName(x.name)">@gitbucket.core.helper.html.checkicon(x.name == branch) @x.name</a></li>
}
}
}

View File

@@ -34,7 +34,7 @@
<div class="head" style="height: 24px;">
<div class="pull-right">
<div class="btn-group">
<a href="@{helpers.url(repository)}/archive@if(pathList.length > 0){/@pathList.map(helpers.urlEncode).mkString("/")}/@{helpers.urlEncode(branch)}.zip" class="btn btn-sm btn-default pc"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
<a href="@{helpers.url(repository)}/archive/@{helpers.urlEncode(branch)}.zip@if(pathList.nonEmpty){?path=@helpers.urlEncode(pathList.mkString("/"))}" class="btn btn-sm btn-default pc"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t" title="Search files"><i class="octicon octicon-search" aria-label="Search files"></i></a>
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName((branch :: pathList).mkString("/"))" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i> @if(commitCount > 10000){10000+} else {@commitCount} @helpers.plural(commitCount, "commit")</a>
</div>

View File

@@ -1,6 +1,6 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
branch: String,
protection: gitbucket.core.api.ApiBranchProtection,
protection: gitbucket.core.api.ApiBranchProtectionResponse,
knownContexts: Seq[String],
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -22,9 +22,44 @@
</label>
<p class="help-block">Disables force-pushes to this branch and prevents it from being deleted.</p>
</div>
<!--====================================================================-->
<!-- Enforce administrators -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.status.enforcement_level.name!="off") @if(knownContexts.isEmpty){disabled }>
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.enforce_admins.exists(_.enabled))>
<span class="strong">Include administrators</span>
</label>
<p class="help-block">Enforce restrictions even for repository administrators.</p>
</div>
<!--====================================================================-->
<!-- Push restrictions -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="restrictions" onclick="update()" @check(protection.restrictions.isDefined)>
<span class="strong">Restrict users for push</span>
</label>
<p class="help-block">Restrict users who can push to this branch</p>
<div class="js-restrictions_enabled" style="display: none;">
<ul id="restrictions-user-list">
</ul>
@gitbucket.core.helper.html.account("userName-restrictions-user", 200, true, false)
<input type="button" class="btn btn-default add-restrictions-user" value="Add"/>
<div>
<span class="error" id="error-restrictions-user"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Status check -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.required_status_checks.isDefined) @if(knownContexts.isEmpty){disabled }>
<span class="strong">Require status checks to pass before merging</span>
</label>
<p class="help-block">When enabled, commits must first be pushed to another branch, then merged or pushed directly to <b>@branch</b> after status checks have passed.</p>
@@ -48,14 +83,6 @@
}
</div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.status.enforcement_level.name=="everyone")>
<span class="strong">Include administrators</span>
</label>
<p class="help-block">Enforce required status checks for repository administrators.</p>
</div>
</div>
}
</div>
@@ -68,59 +95,114 @@
}
<script>
function getValue(){
var v = {}, contexts=[];
const v = {}, contexts = [];
let restrictions = undefined;
$("input[type=checkbox]:checked").each(function(){
if(this.name === 'contexts'){
if(this.name === 'contexts') {
contexts.push(this.value);
} else if (this.name === 'restrictions') {
restrictions = $('#restrictions-user-list li').map(function(i, e){
return $(e).data('name');
}).get();
} else {
v[this.name] = true;
}
});
if(v.enabled){
return {
enabled: true,
required_status_checks: {
enforcement_level: v.has_required_statuses ? ((v.enforce_for_admins ? 'everyone' : 'non_admins')) : 'off',
contexts: v.has_required_statuses ? contexts : []
}
};
enforce_admins: v.enforce_for_admins,
required_status_checks: v.has_required_statuses ? { contexts: contexts } : undefined,
restrictions: restrictions ? { users: restrictions } : undefined
};
} else {
return {
enabled: false,
required_status_checks: {
enforcement_level: "off",
contexts: []
}
enabled: false
};
}
}
function updateView(protection){
$('.js-enabled').toggle(protection.enabled);
$('.js-has_required_statuses').toggle(protection.required_status_checks.enforcement_level != 'off');
$('.js-submit-btn').attr('disabled',protection.required_status_checks.enforcement_level != 'off' && protection.required_status_checks.contexts.length == 0);
$('.js-restrictions_enabled').toggle(protection.restrictions !== undefined);
$('.js-has_required_statuses').toggle(protection.required_status_checks !== undefined);
}
function update(){
var protection = getValue();
const protection = getValue();
updateView(protection);
}
$(update);
function submitForm(e){
e.stopPropagation();
e.preventDefault();
var protection = getValue();
const protection = getValue();
$.ajax({
method:'PATCH',
url:'@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)',
method: 'PATCH',
url: '@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)',
contentType: 'application/json',
dataType: 'json',
data:JSON.stringify({protection:protection}),
success:function(r){
data: JSON.stringify({protection: protection}),
success: function(r){
$('#saved-info').show();
},
error:function(err){
error: function(err){
console.log(err);
alert('update error');
}
});
}
function addUserToListHTML(userName, id){
$(id).append($('<li>').data('name', userName)
.append(' ')
.append(userName)
.append($('<a href="#" onclick="$(this).parent().remove();" class="remove">(remove)</a>')));
}
$(function() {
// Initialize
update();
@protection.restrictions.map(_.users).map { users =>
@users.map { user =>
addUserToListHTML('@user', '#restrictions-user-list');
}
}
$('.add-restrictions-user').click(function(){
$('#error-restrictions-user').text('');
const userName = $('#userName-restrictions-user').val();
// check empty
if($.trim(userName) === ''){
return false;
}
// check duplication
const exists = $('#restrictions-user-list li').filter(function(){
return $(this).data('name') === userName;
}).length > 0;
if(exists){
$('#error-restrictions-user').text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', {
'userName': userName,
'owner': '@repository.owner',
'repository': '@repository.name'
},
function(data, status){
if(data !== ''){
addUserToListHTML(userName, '#restrictions-user-list');
$('#userName-restrictions-user').val('');
} else {
$('#error-restrictions-user').text("User does not exist or isn't writable to this repository.");
}
});
});
})
</script>

View File

@@ -183,14 +183,14 @@ class ApiIntegrationTest extends AnyFunSuite {
.branch("main")
.content("create")
.message("Create content")
.path("README.md")
.path("test.txt")
.commit()
assert(createResult.getContent.isFile == true)
assert(createResult.getContent.isFile)
assert(IOUtils.toString(createResult.getContent.read(), "UTF-8") == "create")
val content1 = repo.getFileContent("README.md")
assert(content1.isFile == true)
val content1 = repo.getFileContent("test.txt")
assert(content1.isFile)
assert(IOUtils.toString(content1.read(), "UTF-8") == "create")
assert(content1.getSha == createResult.getContent.getSha)
@@ -200,14 +200,14 @@ class ApiIntegrationTest extends AnyFunSuite {
.branch("main")
.content("update")
.message("Update content")
.path("README.md")
.path("test.txt")
.sha(content1.getSha)
.commit()
assert(updateResult.getContent.isFile == true)
assert(updateResult.getContent.isFile)
assert(IOUtils.toString(updateResult.getContent.read(), "UTF-8") == "update")
val content2 = repo.getFileContent("README.md")
val content2 = repo.getFileContent("test.txt")
assert(content2.isFile == true)
assert(IOUtils.toString(content2.read(), "UTF-8") == "update")
assert(content2.getSha == updateResult.getContent.getSha)

View File

@@ -1,8 +1,9 @@
package gitbucket.core.api
import java.util.{Calendar, Date, TimeZone}
import gitbucket.core.api.ApiBranchProtectionResponse.Restrictions
import gitbucket.core.model._
import java.util.{Calendar, Date, TimeZone}
import gitbucket.core.model.*
import gitbucket.core.plugin.PluginInfo
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchInfo
import gitbucket.core.service.RepositoryService.RepositoryInfo
@@ -15,7 +16,7 @@ object ApiSpecModels {
implicit val context: JsonFormat.Context = JsonFormat.Context("http://gitbucket.exmple.com", None)
val date1 = {
val date1: Date = {
val d = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
d.set(2011, 3, 14, 16, 0, 49)
d.getTime
@@ -29,7 +30,7 @@ object ApiSpecModels {
// Models
val account = Account(
val account: Account = Account(
userName = "octocat",
fullName = "octocat",
mailAddress = "octocat@example.com",
@@ -45,10 +46,10 @@ object ApiSpecModels {
description = None
)
val sha1 = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
val repo1Name = RepositoryName("octocat/Hello-World")
val sha1: String = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
val repo1Name: RepositoryName = RepositoryName("octocat/Hello-World")
val repository = Repository(
val repository: Repository = Repository(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
isPrivate = false,
@@ -73,7 +74,7 @@ object ApiSpecModels {
)
)
val repositoryInfo = RepositoryInfo(
val repositoryInfo: RepositoryInfo = RepositoryInfo(
owner = repo1Name.owner,
name = repo1Name.name,
repository = repository,
@@ -101,7 +102,7 @@ object ApiSpecModels {
managers = Seq("myboss")
)
val label = Label(
val label: Label = Label(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
labelId = 10,
@@ -109,7 +110,7 @@ object ApiSpecModels {
color = "f29513"
)
val issue = Issue(
val issue: Issue = Issue(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
issueId = 1347,
@@ -124,14 +125,14 @@ object ApiSpecModels {
isPullRequest = false
)
val issuePR = issue.copy(
val issuePR: Issue = issue.copy(
title = "new-feature",
content = Some("Please pull these awesome changes"),
closed = true,
isPullRequest = true
)
val issueComment = IssueComment(
val issueComment: IssueComment = IssueComment(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
issueId = issue.issueId,
@@ -143,7 +144,7 @@ object ApiSpecModels {
updatedDate = date1
)
val pullRequest = PullRequest(
val pullRequest: PullRequest = PullRequest(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
issueId = issuePR.issueId,
@@ -156,7 +157,7 @@ object ApiSpecModels {
isDraft = true
)
val commitComment = CommitComment(
val commitComment: CommitComment = CommitComment(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
commitId = sha1,
@@ -174,7 +175,7 @@ object ApiSpecModels {
originalNewLine = None
)
val commitStatus = CommitStatus(
val commitStatus: CommitStatus = CommitStatus(
commitStatusId = 1,
userName = repo1Name.owner,
repositoryName = repo1Name.name,
@@ -188,7 +189,7 @@ object ApiSpecModels {
updatedDate = date1
)
val milestone = Milestone(
val milestone: Milestone = Milestone(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
milestoneId = 1,
@@ -200,28 +201,28 @@ object ApiSpecModels {
// APIs
val apiUser = ApiUser(account)
val apiUser: ApiUser = ApiUser(account)
val apiRepository = ApiRepository(
val apiRepository: ApiRepository = ApiRepository(
repository = repository,
owner = apiUser,
forkedCount = repositoryInfo.forkedCount,
watchers = 0
)
val apiLabel = ApiLabel(
val apiLabel: ApiLabel = ApiLabel(
label = label,
repositoryName = repo1Name
)
val apiMilestone = ApiMilestone(
val apiMilestone: ApiMilestone = ApiMilestone(
repository = repository,
milestone = milestone,
open_issue_count = 1,
closed_issue_count = 1
)
val apiIssue = ApiIssue(
val apiIssue: ApiIssue = ApiIssue(
issue = issue,
repositoryName = repo1Name,
user = apiUser,
@@ -230,7 +231,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone)
)
val apiNotAssignedIssue = ApiIssue(
val apiNotAssignedIssue: ApiIssue = ApiIssue(
issue = issue,
repositoryName = repo1Name,
user = apiUser,
@@ -239,7 +240,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone)
)
val apiIssuePR = ApiIssue(
val apiIssuePR: ApiIssue = ApiIssue(
issue = issuePR,
repositoryName = repo1Name,
user = apiUser,
@@ -248,7 +249,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone)
)
val apiComment = ApiComment(
val apiComment: ApiComment = ApiComment(
comment = issueComment,
repositoryName = repo1Name,
issueId = issueComment.issueId,
@@ -256,7 +257,7 @@ object ApiSpecModels {
isPullRequest = false
)
val apiCommentPR = ApiComment(
val apiCommentPR: ApiComment = ApiComment(
comment = issueComment,
repositoryName = repo1Name,
issueId = issueComment.issueId,
@@ -264,7 +265,7 @@ object ApiSpecModels {
isPullRequest = true
)
val apiPullRequest = ApiPullRequest(
val apiPullRequest: ApiPullRequest = ApiPullRequest(
issue = issuePR,
pullRequest = pullRequest,
headRepo = apiRepository,
@@ -275,15 +276,14 @@ object ApiSpecModels {
mergedComment = Some((issueComment, account))
)
// https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
val apiPullRequestReviewComment = ApiPullRequestReviewComment(
val apiPullRequestReviewComment: ApiPullRequestReviewComment = ApiPullRequestReviewComment(
comment = commitComment,
commentedUser = apiUser,
repositoryName = repo1Name,
issueId = commitComment.issueId.get
)
val commitInfo = (id: String) =>
val commitInfo: String => CommitInfo = (id: String) =>
CommitInfo(
id = id,
shortMessage = "short message",
@@ -299,12 +299,12 @@ object ApiSpecModels {
None
)
val apiCommitListItem = ApiCommitListItem(
val apiCommitListItem: ApiCommitListItem = ApiCommitListItem(
commit = commitInfo(sha1),
repositoryName = repo1Name
)
val apiCommit = {
val apiCommit: ApiCommit = {
val commit = commitInfo(sha1)
ApiCommit(
id = commit.id,
@@ -318,7 +318,7 @@ object ApiSpecModels {
)(repo1Name)
}
val apiCommits = ApiCommits(
val apiCommits: ApiCommits = ApiCommits(
repositoryName = repo1Name,
commitInfo = commitInfo(sha1),
diffs = Seq(
@@ -348,42 +348,45 @@ object ApiSpecModels {
commentCount = 2
)
val apiCommitStatus = ApiCommitStatus(
val apiCommitStatus: ApiCommitStatus = ApiCommitStatus(
status = commitStatus,
creator = apiUser
)
val apiCombinedCommitStatus = ApiCombinedCommitStatus(
val apiCombinedCommitStatus: ApiCombinedCommitStatus = ApiCombinedCommitStatus(
sha = sha1,
statuses = Iterable((commitStatus, account)),
repository = apiRepository
)
val apiBranchProtectionOutput = ApiBranchProtection(
val apiBranchProtectionOutput: ApiBranchProtectionResponse = ApiBranchProtectionResponse(
info = ProtectedBranchInfo(
owner = repo1Name.owner,
repository = repo1Name.name,
branch = "main",
enabled = true,
contexts = Seq("continuous-integration/travis-ci"),
includeAdministrators = true
contexts = Some(Seq("continuous-integration/travis-ci")),
enforceAdmins = true,
restrictionsUsers = Some(Seq("admin"))
)
)
val apiBranchProtectionInput = new ApiBranchProtection(
val apiBranchProtectionInput: ApiBranchProtectionResponse = new ApiBranchProtectionResponse(
url = None,
enabled = true,
required_status_checks = Some(
ApiBranchProtection.Status(
ApiBranchProtectionResponse.Status(
url = None,
enforcement_level = ApiBranchProtection.Everyone,
enforcement_level = ApiBranchProtectionResponse.Everyone,
contexts = Seq("continuous-integration/travis-ci"),
contexts_url = None
)
)
),
restrictions = Some(Restrictions(users = Seq("admin"))),
enforce_admins = None
)
val apiBranch = ApiBranch(
val apiBranch: ApiBranch = ApiBranch(
name = "main",
commit = ApiBranchCommit(sha1),
protection = apiBranchProtectionOutput
@@ -391,12 +394,12 @@ object ApiSpecModels {
repositoryName = repo1Name
)
val apiBranchForList = ApiBranchForList(
val apiBranchForList: ApiBranchForList = ApiBranchForList(
name = "main",
commit = ApiBranchCommit(sha1)
)
val apiContents = ApiContents(
val apiContents: ApiContents = ApiContents(
fileInfo = FileInfo(
id = ObjectId.fromString(sha1),
isDirectory = false,
@@ -413,14 +416,14 @@ object ApiSpecModels {
content = Some("README".getBytes("UTF-8"))
)
val apiEndPoint = ApiEndPoint()
val apiEndPoint: ApiEndPoint = ApiEndPoint()
val apiError = ApiError(
val apiError: ApiError = ApiError(
message = "A repository with this name already exists on this account",
documentation_url = Some("https://developer.github.com/v3/repos/#create")
)
val apiGroup = ApiGroup(
val apiGroup: ApiGroup = ApiGroup(
account.copy(
isAdmin = true,
isGroupAccount = true,
@@ -428,7 +431,7 @@ object ApiSpecModels {
)
)
val apiPlugin = ApiPlugin(
val apiPlugin: ApiPlugin = ApiPlugin(
plugin = PluginInfo(
pluginId = "gist",
pluginName = "Gist Plugin",
@@ -441,12 +444,12 @@ object ApiSpecModels {
)
)
val apiPusher = ApiPusher(account)
val apiPusher: ApiPusher = ApiPusher(account)
// 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 gitHubContext: JsonFormat.Context = JsonFormat.Context("https://api.github.com", Some("https://api.github.com"))
val apiRefHeadsMaster = ApiRef(
val apiRefHeadsMaster: ApiRef = ApiRef(
ref = "refs/heads/main",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/heads/main"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
@@ -457,7 +460,7 @@ object ApiSpecModels {
)
)
val apiRefTag = ApiRef(
val apiRefTag: ApiRef = ApiRef(
ref = "refs/tags/1.0",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/tags/1.0"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
@@ -468,9 +471,9 @@ object ApiSpecModels {
)
)
val assetFileName = "010203040a0b0c0d"
val assetFileName: String = "010203040a0b0c0d"
val apiReleaseAsset = ApiReleaseAsset(
val apiReleaseAsset: ApiReleaseAsset = ApiReleaseAsset(
name = "release.zip",
size = 100
)(
@@ -479,7 +482,7 @@ object ApiSpecModels {
repositoryName = repo1Name
)
val apiRelease = ApiRelease(
val apiRelease: ApiRelease = ApiRelease(
name = "release1",
tag_name = "tag1",
body = Some("content"),
@@ -489,7 +492,7 @@ object ApiSpecModels {
// JSON String for APIs
val jsonUser = """{
val jsonUser: String = """{
|"login":"octocat",
|"email":"octocat@example.com",
|"type":"User",
@@ -501,7 +504,7 @@ object ApiSpecModels {
|"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar"
|}""".stripMargin
val jsonRepository = s"""{
val jsonRepository: String = s"""{
|"name":"Hello-World",
|"full_name":"octocat/Hello-World",
|"description":"This your first repo!",
@@ -519,10 +522,10 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World"
|}""".stripMargin
val jsonLabel =
val jsonLabel: String =
"""{"name":"bug","color":"f29513","url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/labels/bug"}"""
val jsonMilestone = """{
val jsonMilestone: String = """{
|"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,
@@ -535,7 +538,7 @@ object ApiSpecModels {
|"due_on":"2011-04-14T16:00:49Z"
|}""".stripMargin
val jsonIssue = s"""{
val jsonIssue: String = s"""{
|"number":1347,
|"title":"Found a bug",
|"user":$jsonUser,
@@ -552,7 +555,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin
val jsonNotAssignedIssue = s"""{
val jsonNotAssignedIssue: String = s"""{
|"number":1347,
|"title":"Found a bug",
|"user":$jsonUser,
@@ -568,7 +571,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin
val jsonIssuePR = s"""{
val jsonIssuePR: String = s"""{
|"number":1347,
|"title":"new-feature",
|"user":$jsonUser,
@@ -588,7 +591,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"}
|}""".stripMargin
val jsonPullRequest = s"""{
val jsonPullRequest: String = s"""{
|"number":1347,
|"state":"closed",
|"updated_at":"2011-04-14T16:00:49Z",
@@ -615,7 +618,7 @@ object ApiSpecModels {
|"statuses_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e"
|}""".stripMargin
val jsonPullRequestReviewComment = s"""{
val jsonPullRequestReviewComment: String = s"""{
|"id":29724692,
|"path":"README.md",
|"commit_id":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -632,7 +635,7 @@ object ApiSpecModels {
|"pull_request":{"href":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347"}}
|}""".stripMargin
val jsonComment = s"""{
val jsonComment: String = s"""{
|"id":1,
|"user":$jsonUser,
|"body":"Me too",
@@ -641,7 +644,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347#comment-1"
|}""".stripMargin
val jsonCommentPR = s"""{
val jsonCommentPR: String = s"""{
|"id":1,
|"user":$jsonUser,
|"body":"Me too",
@@ -650,7 +653,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347#comment-1"
|}""".stripMargin
val jsonCommitListItem = s"""{
val jsonCommitListItem: String = s"""{
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"commit":{
|"message":"full message",
@@ -664,7 +667,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e"
|}""".stripMargin
val jsonCommit = (id: String) => s"""{
val jsonCommit: String => String = (id: String) => s"""{
|"id":"$id",
|"message":"full message",
|"timestamp":"2011-04-14T16:00:49Z",
@@ -677,7 +680,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/$id"
|}""".stripMargin
val jsonCommits = s"""{
val jsonCommits: String = s"""{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -707,7 +710,7 @@ object ApiSpecModels {
|"patch":"@@ -1 +1,2 @@\\n-body1\\n\\\\ No newline at end of file\\n+body1\\n+body2\\n\\\\ No newline at end of file"}]
|}""".stripMargin
val jsonCommitStatus = s"""{
val jsonCommitStatus: String = s"""{
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"state":"success",
@@ -719,7 +722,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses"
|}""".stripMargin
val jsonCombinedCommitStatus = s"""{
val jsonCombinedCommitStatus: String = s"""{
|"state":"success",
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"total_count":1,
@@ -728,7 +731,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/status"
|}""".stripMargin
val jsonBranchProtectionOutput =
val jsonBranchProtectionOutput: String =
"""{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection",
|"enabled":true,
@@ -736,15 +739,25 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks",
|"enforcement_level":"everyone",
|"contexts":["continuous-integration/travis-ci"],
|"contexts_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks/contexts"}
|"contexts_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks/contexts"
|},
|"restrictions":{
|"users":["admin"]
|},
|"enforce_admins":{
|"enabled":true
|}
|}""".stripMargin
val jsonBranchProtectionInput =
val jsonBranchProtectionInput: String =
"""{
|"enabled":true,
|"required_status_checks":{
|"enforcement_level":"everyone",
|"contexts":["continuous-integration/travis-ci"]
|},
|"restrictions":{
|"users":["admin"]
|}
|}""".stripMargin
@@ -757,9 +770,9 @@ object ApiSpecModels {
|"html":"http://gitbucket.exmple.com/octocat/Hello-World/tree/main"}
|}""".stripMargin
val jsonBranchForList = """{"name":"main","commit":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonBranchForList: String = """{"name":"main","commit":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonContents =
val jsonContents: String =
"""{
|"type":"file",
|"name":"README.md",
@@ -770,14 +783,14 @@ object ApiSpecModels {
|"download_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/doc/README.md"
|}""".stripMargin
val jsonEndPoint = """{"rate_limit_url":"http://gitbucket.exmple.com/api/v3/rate_limit"}"""
val jsonEndPoint: String = """{"rate_limit_url":"http://gitbucket.exmple.com/api/v3/rate_limit"}"""
val jsonError = """{
val jsonError: String = """{
|"message":"A repository with this name already exists on this account",
|"documentation_url":"https://developer.github.com/v3/repos/#create"
|}""".stripMargin
val jsonGroup = """{
val jsonGroup: String = """{
|"login":"octocat",
|"description":"Admin group",
|"created_at":"2011-04-14T16:00:49Z",
@@ -787,7 +800,7 @@ object ApiSpecModels {
|"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar"
|}""".stripMargin
val jsonPlugin = """{
val jsonPlugin: String = """{
|"id":"gist",
|"name":"Gist Plugin",
|"version":"4.16.0",
@@ -795,12 +808,12 @@ object ApiSpecModels {
|"jarFileName":"gitbucket-gist-plugin-gitbucket_4.30.0-SNAPSHOT-4.17.0.jar"
|}""".stripMargin
val jsonPusher = """{"name":"octocat","email":"octocat@example.com"}"""
val jsonPusher: String = """{"name":"octocat","email":"octocat@example.com"}"""
// I checked all refs in gitbucket repo, and there appears to be only type "commit" and type "tag"
val jsonRef = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonRef: String = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonRefHeadsMain =
val jsonRefHeadsMain: String =
"""{
|"ref": "refs/heads/main",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
@@ -812,7 +825,7 @@ object ApiSpecModels {
|}
|}""".stripMargin
val jsonRefTag =
val jsonRefTag: String =
"""{
|"ref": "refs/tags/1.0",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
@@ -824,7 +837,7 @@ object ApiSpecModels {
|}
|}""".stripMargin
val jsonReleaseAsset =
val jsonReleaseAsset: String =
s"""{
|"name":"release.zip",
|"size":100,
@@ -833,7 +846,7 @@ object ApiSpecModels {
|"browser_download_url":"http://gitbucket.exmple.com/octocat/Hello-World/releases/tag1/assets/${assetFileName}"
|}""".stripMargin
val jsonRelease =
val jsonRelease: String =
s"""{
|"name":"release1",
|"tag_name":"tag1",

View File

@@ -1,19 +1,19 @@
package gitbucket.core.api
import org.json4s.Formats
import org.json4s.{Formats, JValue, jvalue2extractable}
import org.json4s.jackson.JsonMethods
import org.json4s.jvalue2extractable
import org.scalatest.Assertion
import org.scalatest.funsuite.AnyFunSuite
class JsonFormatSpec extends AnyFunSuite {
import ApiSpecModels._
import ApiSpecModels.*
implicit val format: Formats = JsonFormat.jsonFormats
private def expected(json: String) = json.replaceAll("\n", "")
def normalizeJson(json: String) = {
def normalizeJson(json: String): JValue = {
org.json4s.jackson.parseJson(json)
}
def assertEqualJson(actual: String, expected: String) = {
def assertEqualJson(actual: String, expected: String): Assertion = {
assert(normalizeJson(actual) == normalizeJson(expected))
}
@@ -57,7 +57,9 @@ class JsonFormatSpec extends AnyFunSuite {
assert(JsonFormat(apiBranchProtectionOutput) == expected(jsonBranchProtectionOutput))
}
test("deserialize apiBranchProtection") {
assert(JsonMethods.parse(jsonBranchProtectionInput).extract[ApiBranchProtection] == apiBranchProtectionInput)
assert(
JsonMethods.parse(jsonBranchProtectionInput).extract[ApiBranchProtectionResponse] == apiBranchProtectionInput
)
}
test("apiBranch") {
assert(JsonFormat(apiBranch) == expected(jsonBranch))

View File

@@ -29,26 +29,28 @@ class ProtectedBranchServiceSpec
it("should enable and update and disable") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(
getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo(
"user1",
"repo1",
"branch",
true,
Nil,
false
None,
false,
None
)
)
enableBranchProtection("user1", "repo1", "branch", true, Seq("hoge", "huge"))
enableBranchProtection("user1", "repo1", "branch", true, true, Seq("hoge", "huge"), false, Nil)
assert(
getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo(
"user1",
"repo1",
"branch",
true,
Seq("hoge", "huge"),
true
enabled = true,
contexts = Some(Seq("hoge", "huge")),
enforceAdmins = true,
restrictionsUsers = None
)
)
disableBranchProtection("user1", "repo1", "branch")
@@ -57,21 +59,21 @@ class ProtectedBranchServiceSpec
)
}
}
it("should empty contexts is no-include-administrators") {
it("should empty contexts is include-administrators") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false)
enableBranchProtection("user1", "repo1", "branch", true, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(!getProtectedBranchInfo("user1", "repo1", "branch").enforceAdmins)
enableBranchProtection("user1", "repo1", "branch", true, false, Nil, false, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").enforceAdmins)
}
}
it("getProtectedBranchList") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil)
enableBranchProtection("user1", "repo1", "branch2", false, Seq("fuga"))
enableBranchProtection("user1", "repo1", "branch3", true, Seq("hoge"))
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
enableBranchProtection("user1", "repo1", "branch2", false, false, Seq("fuga"), false, Nil)
enableBranchProtection("user1", "repo1", "branch3", true, false, Seq("hoge"), false, Nil)
assert(getProtectedBranchList("user1", "repo1").toSet == Set("branch", "branch2", "branch3"))
}
}
@@ -87,12 +89,12 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE_NONFASTFORWARD
)
generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"Cannot force-push to a protected branch"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("Cannot force-push to a protected branch")
)
}
}
@@ -109,12 +111,12 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE_NONFASTFORWARD
)
generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"Cannot force-push to a protected branch"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("Cannot force-push to a protected branch")
)
}
}
@@ -131,33 +133,33 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE
)
val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"Required status check \"must\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("Required status check \"must\" is expected")
)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must", "must2"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"Required status check \"must2\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("Required status check \"must2\" is expected")
)
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
}
}
}
@@ -173,37 +175,60 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE
)
val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must"), false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", true, true, Seq("must"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"Required status check \"must\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("Required status check \"must\" is expected")
)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must", "must2"))
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must", "must2"), false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", true, true, Seq("must", "must2"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"Required status check \"must2\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("Required status check \"must2\" is expected")
)
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
}
}
}
it("should restrict push to allowed users only") {
withTestDB { implicit session =>
withTestRepository { git =>
val rp = new ReceivePack(git.getRepository)
rp.setAllowNonFastForwards(true)
val rc = new ReceiveCommand(
ObjectId.fromString(sha),
ObjectId.fromString(sha2),
"refs/heads/branch",
ReceiveCommand.Type.UPDATE
)
generateNewUserWithDBRepository("user1", "repo1")
generateNewAccount("user2")
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, true, Seq("user2"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
assert(
receiveHook
.preReceive("user1", "repo1", rp, rc, "user3", false)
.contains("You do not have permission to push to this branch")
)
}
}
}
@@ -212,29 +237,53 @@ class ProtectedBranchServiceSpec
it("administrator is owner") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, Nil, false)
assert(x.isAdministrator("user1") == true)
assert(x.isAdministrator("user2") == false)
val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
assert(x.isAdministrator("user1"))
assert(!x.isAdministrator("user2"))
}
}
it("administrator is manager") {
withTestDB { implicit session =>
val x = ProtectedBranchInfo("grp1", "repo1", "branch", true, Nil, false)
val x = ProtectedBranchInfo(
"grp1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
x.createGroup("grp1", None, None)
generateNewAccount("user1")
generateNewAccount("user2")
generateNewAccount("user3")
x.updateGroupMembers("grp1", List("user1" -> true, "user2" -> false))
assert(x.isAdministrator("user1") == true)
assert(x.isAdministrator("user2") == false)
assert(x.isAdministrator("user3") == false)
assert(x.isAdministrator("user1"))
assert(!x.isAdministrator("user2"))
assert(!x.isAdministrator("user3"))
}
}
it("unSuccessedContexts") {
withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, List("must"), false)
val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(List("must")),
enforceAdmins = false,
restrictionsUsers = None
)
assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
assert(x.unSuccessedContexts(sha) == Set("must"))
@@ -251,7 +300,15 @@ class ProtectedBranchServiceSpec
it("unSuccessedContexts when empty") {
withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, Nil, false)
val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
val sha = "0c77148632618b59b6f70004e3084002be2b8804"
assert(x.unSuccessedContexts(sha) == Set())
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
@@ -261,23 +318,63 @@ class ProtectedBranchServiceSpec
it("if disabled, needStatusCheck is false") {
withTestDB { implicit session =>
assert(
ProtectedBranchInfo("user1", "repo1", "branch", false, Seq("must"), true).needStatusCheck("user1") == false
!ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = false,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user1")
)
}
}
it("needStatusCheck includeAdministrators") {
withTestDB { implicit session =>
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), false).needStatusCheck("user2") == true
ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = false,
restrictionsUsers = None
).needStatusCheck("user2")
)
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), false).needStatusCheck("user1") == false
!ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = false,
restrictionsUsers = None
).needStatusCheck("user1")
)
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), true).needStatusCheck("user2") == true
ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user2")
)
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), true).needStatusCheck("user1") == true
ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user1")
)
}
}

View File

@@ -21,7 +21,7 @@ class RepositoryServiceSpec extends AnyFunSuite with ServiceSpecBase with Reposi
now = new java.util.Date
)
service.enableBranchProtection("root", "repo", "branch", true, Seq("must1", "must2"))
service.enableBranchProtection("root", "repo", "branch", true, true, Seq("must1", "must2"), false, Nil)
val orgPbi = service.getProtectedBranchInfo("root", "repo", "branch")
val org = service.getCommitStatus("root", "repo", id).get