mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-01 19:15:59 +01:00
Compare commits
46 Commits
revert_pul
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a79142074f | ||
|
|
9f58c6dce7 | ||
|
|
6fe903afab | ||
|
|
0d94865633 | ||
|
|
2fbdead64b | ||
|
|
72a354931e | ||
|
|
cb8affcd0d | ||
|
|
a5a997eb40 | ||
|
|
b4220aab68 | ||
|
|
c8a4798f86 | ||
|
|
6b8e9e8892 | ||
|
|
9ab9363d0b | ||
|
|
1edb18a147 | ||
|
|
d8b4bf3033 | ||
|
|
6597c4490b | ||
|
|
daaf1696ad | ||
|
|
53a1ca7874 | ||
|
|
f045fa4c6a | ||
|
|
53a7c5adf8 | ||
|
|
b142eca9a5 | ||
|
|
ba753a373b | ||
|
|
95ceca75a4 | ||
|
|
f16cc117a9 | ||
|
|
af66f8f746 | ||
|
|
d9cc57e8e0 | ||
|
|
72b6dad3a2 | ||
|
|
18f396b4a2 | ||
|
|
9f3fde8de2 | ||
|
|
dfd6f80b63 | ||
|
|
119d91210c | ||
|
|
75ef30ee03 | ||
|
|
d1cf9dd600 | ||
|
|
9c9fea908c | ||
|
|
1145c4d0f6 | ||
|
|
d847fc6e0f | ||
|
|
bb9585f7a6 | ||
|
|
e91411fa45 | ||
|
|
adbc065a6f | ||
|
|
862283b729 | ||
|
|
217df7012c | ||
|
|
e672d41e77 | ||
|
|
d975700bd4 | ||
|
|
f65e41561a | ||
|
|
dab4f33ed9 | ||
|
|
5ff45ef5ae | ||
|
|
af7c622647 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -9,9 +9,9 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java: [17, 21]
|
java: [17, 25]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Cache
|
- name: Cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
~/.cache/coursier/v1
|
~/.cache/coursier/v1
|
||||||
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
|
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java }}
|
java-version: ${{ matrix.java }}
|
||||||
distribution: adopt
|
distribution: adopt
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Build executable
|
- name: Build executable
|
||||||
run: sbt executable
|
run: sbt executable
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
|
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
|
||||||
path: ./target/executable/gitbucket.*
|
path: ./target/executable/gitbucket.*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version = "3.9.9"
|
version = "3.10.1"
|
||||||
project.git = true
|
project.git = true
|
||||||
|
|
||||||
maxColumn = 120
|
maxColumn = 120
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All changes to the project will be documented in this file.
|
All changes to the project will be documented in this file.
|
||||||
|
|
||||||
|
## 4.44.0 - 23 Sep 2025
|
||||||
|
- Enhanced branch protection which supports the following settings:
|
||||||
|
- Prevent pushes from non-allowed users
|
||||||
|
- Whether to apply restrictions to administrator users as well
|
||||||
|
- Improve logging for initialization errors
|
||||||
|
|
||||||
## 4.43.0 - 29 Jun 2025
|
## 4.43.0 - 29 Jun 2025
|
||||||
- Upgrade H2 database from 1.x to 2.x
|
- Upgrade H2 database from 1.x to 2.x
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -44,7 +44,7 @@ GitBucket has a plug-in system that allows extra functionality. Officially the f
|
|||||||
- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin)
|
- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin)
|
||||||
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin)
|
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin)
|
||||||
|
|
||||||
You can find more plugins made by the community at [GitBucket community plugins](https://gitbucket-plugins.github.io/).
|
You can find more plugins made by the community at [GitBucket community plugins](https://github.com/gitbucket/gitbucket/wiki/Community-Plugins).
|
||||||
|
|
||||||
Building and Development
|
Building and Development
|
||||||
-----------
|
-----------
|
||||||
@@ -59,12 +59,15 @@ Support
|
|||||||
- 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.
|
- 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.
|
- 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
|
## 4.44.0 - 23 Sep 2025
|
||||||
- Upgrade H2 database from 1.x to 2.x
|
- Enhanced branch protection which supports the following settings:
|
||||||
|
- Prevent pushes from non-allowed users
|
||||||
|
- Whether to apply restrictions to administrator users as well
|
||||||
|
- 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:
|
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
|
```bash
|
||||||
|
|||||||
65
build.sbt
65
build.sbt
@@ -2,9 +2,9 @@ import com.jsuereth.sbtpgp.PgpKeys._
|
|||||||
|
|
||||||
val Organization = "io.github.gitbucket"
|
val Organization = "io.github.gitbucket"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val GitBucketVersion = "4.43.0"
|
val GitBucketVersion = "4.44.0"
|
||||||
val ScalatraVersion = "3.1.2"
|
val ScalatraVersion = "3.1.2"
|
||||||
val JettyVersion = "10.0.25"
|
val JettyVersion = "10.0.26"
|
||||||
val JgitVersion = "6.10.1.202505221210-r"
|
val JgitVersion = "6.10.1.202505221210-r"
|
||||||
|
|
||||||
lazy val root = (project in file("."))
|
lazy val root = (project in file("."))
|
||||||
@@ -14,9 +14,9 @@ sourcesInBase := false
|
|||||||
organization := Organization
|
organization := Organization
|
||||||
name := Name
|
name := Name
|
||||||
version := GitBucketVersion
|
version := GitBucketVersion
|
||||||
scalaVersion := "2.13.16"
|
scalaVersion := "2.13.17"
|
||||||
|
|
||||||
crossScalaVersions += "3.7.2"
|
crossScalaVersions += "3.7.3"
|
||||||
|
|
||||||
// scalafmtOnCompile := true
|
// scalafmtOnCompile := true
|
||||||
|
|
||||||
@@ -37,38 +37,37 @@ libraryDependencies ++= Seq(
|
|||||||
"org.apache.commons" % "commons-email" % "1.6.0",
|
"org.apache.commons" % "commons-email" % "1.6.0",
|
||||||
"commons-net" % "commons-net" % "3.12.0",
|
"commons-net" % "commons-net" % "3.12.0",
|
||||||
"org.apache.httpcomponents" % "httpclient" % "4.5.14",
|
"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",
|
"org.apache.sshd",
|
||||||
"sshd-mina"
|
"sshd-mina"
|
||||||
) exclude ("org.apache.sshd", "sshd-netty")
|
) exclude ("org.apache.sshd", "sshd-netty")
|
||||||
exclude ("org.apache.sshd", "sshd-spring-sftp"),
|
exclude ("org.apache.sshd", "sshd-spring-sftp"),
|
||||||
"org.apache.tika" % "tika-core" % "3.2.2",
|
"org.apache.tika" % "tika-core" % "3.2.3",
|
||||||
"com.github.takezoe" %% "blocking-slick" % "0.0.14",
|
"com.github.takezoe" %% "blocking-slick" % "0.0.14",
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
"com.h2database" % "h2" % "2.3.232",
|
"com.h2database" % "h2" % "2.4.240",
|
||||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.12",
|
"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",
|
"ch.qos.logback" % "logback-classic" % "1.5.20",
|
||||||
"com.zaxxer" % "HikariCP" % "7.0.1" exclude ("org.slf4j", "slf4j-api"),
|
"com.zaxxer" % "HikariCP" % "7.0.2" exclude ("org.slf4j", "slf4j-api"),
|
||||||
"com.typesafe" % "config" % "1.4.4",
|
"com.typesafe" % "config" % "1.4.5",
|
||||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
|
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
|
||||||
"io.github.java-diff-utils" % "java-diff-utils" % "4.16",
|
"io.github.java-diff-utils" % "java-diff-utils" % "4.16",
|
||||||
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
|
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
|
||||||
"net.coobird" % "thumbnailator" % "0.4.20",
|
"net.coobird" % "thumbnailator" % "0.4.21",
|
||||||
"com.github.zafarkhaja" % "java-semver" % "0.10.2",
|
"com.github.zafarkhaja" % "java-semver" % "0.10.2",
|
||||||
"com.nimbusds" % "oauth2-oidc-sdk" % "11.27",
|
"com.nimbusds" % "oauth2-oidc-sdk" % "11.30",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||||
"junit" % "junit" % "4.13.2" % "test",
|
"junit" % "junit" % "4.13.2" % "test",
|
||||||
"org.scalatra" %% "scalatra-scalatest-javax" % ScalatraVersion % "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" % "testcontainers-mysql" % "2.0.1" % "test",
|
||||||
"org.testcontainers" % "mysql" % "1.21.3" % "test",
|
"org.testcontainers" % "testcontainers-postgresql" % "2.0.1" % "test",
|
||||||
"org.testcontainers" % "postgresql" % "1.21.3" % "test",
|
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
||||||
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
||||||
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
"org.ec4j.core" % "ec4j-core" % "1.1.1",
|
||||||
"org.ec4j.core" % "ec4j-core" % "1.1.1",
|
"org.kohsuke" % "github-api" % "1.330" % "test"
|
||||||
"org.kohsuke" % "github-api" % "1.329" % "test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compiler settings
|
// Compiler settings
|
||||||
@@ -192,7 +191,7 @@ executableKey := {
|
|||||||
|
|
||||||
// zip it up
|
// zip it up
|
||||||
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
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)
|
IO.relativizeFile(temp, file)
|
||||||
}
|
}
|
||||||
val manifest = new JarManifest
|
val manifest = new JarManifest
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Generate release files
|
|||||||
|
|
||||||
For plug-in development, we have to publish the GitBucket jar file to the Maven central repository before release GitBucket itself.
|
For plug-in development, we have to publish the GitBucket jar file to the Maven central repository before release GitBucket itself.
|
||||||
|
|
||||||
First, start the sbt shell:
|
First, stage artifacts on your machine:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sbt publishSigned
|
$ sbt publishSigned
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
sbt.version=1.11.4
|
sbt.version=1.11.7
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||||
|
|
||||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5")
|
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6")
|
||||||
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.9")
|
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.9")
|
||||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1")
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1")
|
||||||
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
|
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
|
||||||
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
|
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.9.0")
|
||||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1")
|
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.0")
|
||||||
|
|
||||||
addDependencyTreePlugin
|
addDependencyTreePlugin
|
||||||
|
|||||||
@@ -237,7 +237,7 @@
|
|||||||
<addForeignKeyConstraint constraintName="IDX_ISSUE_ID_FK1" baseTableName="ISSUE_ID" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
<addForeignKeyConstraint constraintName="IDX_ISSUE_ID_FK1" baseTableName="ISSUE_ID" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
|
||||||
<!--================================================================================================-->
|
<!--================================================================================================-->
|
||||||
<!-- ISSUE_LABEL -->
|
<!-- ISSUE_ID -->
|
||||||
<!--================================================================================================-->
|
<!--================================================================================================-->
|
||||||
<createTable tableName="ISSUE_LABEL">
|
<createTable tableName="ISSUE_LABEL">
|
||||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
|||||||
@@ -29,12 +29,4 @@
|
|||||||
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_PK" tableName="PROTECTED_BRANCH_RESTRICTION" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, ALLOWED_USER"/>
|
<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_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"/>
|
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK1" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="ALLOWED_USER" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
|
||||||
<!--================================================================================================-->
|
|
||||||
<!-- PULL_REQUEST -->
|
|
||||||
<!--================================================================================================-->
|
|
||||||
<addColumn tableName="PULL_REQUEST">
|
|
||||||
<column name="MERGED_COMMIT_IDS" type="text" nullable="true"/>
|
|
||||||
</addColumn>
|
|
||||||
|
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ case class Context(
|
|||||||
val path: String = settings.baseUrl.getOrElse(request.getContextPath)
|
val path: String = settings.baseUrl.getOrElse(request.getContextPath)
|
||||||
val currentPath: String = request.getRequestURI.substring(request.getContextPath.length)
|
val currentPath: String = request.getRequestURI.substring(request.getContextPath.length)
|
||||||
val baseUrl: String = settings.baseUrl(request)
|
val baseUrl: String = settings.baseUrl(request)
|
||||||
val host: String = new java.net.URL(baseUrl).getHost
|
val host: String = new java.net.URI(baseUrl).toURL.getHost
|
||||||
val platform: String = request.getHeader("User-Agent") match {
|
val platform: String = request.getHeader("User-Agent") match {
|
||||||
case null => null
|
case null => null
|
||||||
case agent if agent.contains("Mac") => "mac"
|
case agent if agent.contains("Mac") => "mac"
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ import gitbucket.core.util.Implicits.*
|
|||||||
import gitbucket.core.util.*
|
import gitbucket.core.util.*
|
||||||
import org.scalatra.forms.*
|
import org.scalatra.forms.*
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.revwalk.RevWalk
|
|
||||||
import org.scalatra.BadRequest
|
import org.scalatra.BadRequest
|
||||||
|
|
||||||
import java.nio.file.Files
|
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
|
||||||
class PullRequestsController
|
class PullRequestsController
|
||||||
@@ -249,43 +247,41 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/pull/:id/delete_branch")(readableUsersOnly { baseRepository =>
|
get("/:owner/:repository/pull/:id/delete_branch")(readableUsersOnly { baseRepository =>
|
||||||
context.withLoginAccount { _ =>
|
(for {
|
||||||
(for {
|
issueId <- params("id").toIntOpt
|
||||||
issueId <- params("id").toIntOpt
|
loginAccount <- context.loginAccount
|
||||||
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
|
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
|
||||||
owner = pullreq.requestUserName
|
owner = pullreq.requestUserName
|
||||||
name = pullreq.requestRepositoryName
|
name = pullreq.requestRepositoryName
|
||||||
if hasDeveloperRole(owner, name, context.loginAccount)
|
if hasDeveloperRole(owner, name, context.loginAccount)
|
||||||
} yield {
|
} yield {
|
||||||
val repository = getRepository(owner, name).get
|
val repository = getRepository(owner, name).get
|
||||||
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
|
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
|
||||||
if (branchProtection.enabled) {
|
if (branchProtection.enabled) {
|
||||||
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
|
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
|
||||||
} else {
|
} else {
|
||||||
if (repository.repository.defaultBranch != pullreq.requestBranch) {
|
if (repository.repository.defaultBranch != pullreq.requestBranch) {
|
||||||
val userName = context.loginAccount.get.userName
|
val userName = context.loginAccount.get.userName
|
||||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
|
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
|
||||||
val deleteBranchInfo =
|
val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
|
||||||
DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
|
recordActivity(deleteBranchInfo)
|
||||||
recordActivity(deleteBranchInfo)
|
|
||||||
}
|
|
||||||
createComment(
|
|
||||||
baseRepository.owner,
|
|
||||||
baseRepository.name,
|
|
||||||
userName,
|
|
||||||
issueId,
|
|
||||||
pullreq.requestBranch,
|
|
||||||
"delete_branch"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
|
|
||||||
}
|
}
|
||||||
|
createComment(
|
||||||
|
baseRepository.owner,
|
||||||
|
baseRepository.name,
|
||||||
|
userName,
|
||||||
|
issueId,
|
||||||
|
pullreq.requestBranch,
|
||||||
|
"delete_branch"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
|
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
|
||||||
}) getOrElse NotFound()
|
}) getOrElse NotFound()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/pull/:id/update_branch")(readableUsersOnly { baseRepository =>
|
post("/:owner/:repository/pull/:id/update_branch")(readableUsersOnly { baseRepository =>
|
||||||
@@ -365,11 +361,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
form.isDraft,
|
form.isDraft,
|
||||||
context.settings
|
context.settings
|
||||||
) match {
|
) match {
|
||||||
case Right(result) =>
|
case Right(objectId) => redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
|
||||||
updateMergedCommitIds(repository.owner, repository.name, issueId, result.mergedCommitId)
|
case Left(message) => Some(BadRequest(message))
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
|
|
||||||
case Left(message) =>
|
|
||||||
Some(BadRequest(message))
|
|
||||||
}
|
}
|
||||||
} getOrElse NotFound()
|
} getOrElse NotFound()
|
||||||
}
|
}
|
||||||
@@ -729,107 +722,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/:owner/:repository/pull/:id/revert")(writableUsersOnly { repository =>
|
|
||||||
context.withLoginAccount { loginAccount =>
|
|
||||||
(for {
|
|
||||||
issueId <- params.get("id").map(_.toInt)
|
|
||||||
(issue, pullreq) <- getPullRequest(repository.owner, repository.name, issueId) if issue.closed
|
|
||||||
} yield {
|
|
||||||
val baseBranch = pullreq.branch
|
|
||||||
val revertBranch = s"revert-pr-$issueId-${System.currentTimeMillis()}"
|
|
||||||
|
|
||||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
|
||||||
try {
|
|
||||||
// Create a new branch from base
|
|
||||||
JGitUtil.createBranch(git, baseBranch, revertBranch)
|
|
||||||
// TODO Call webhook ???
|
|
||||||
|
|
||||||
val tempDir = Files.createTempDirectory("jgit-revert-")
|
|
||||||
val revertCommitName =
|
|
||||||
try {
|
|
||||||
// Clone bare repository
|
|
||||||
Using.resource(
|
|
||||||
Git.cloneRepository
|
|
||||||
.setURI(getRepositoryDir(repository.owner, repository.name).getAbsolutePath)
|
|
||||||
.setDirectory(tempDir.toFile)
|
|
||||||
.setBranch(revertBranch)
|
|
||||||
.setBare(false)
|
|
||||||
.setNoCheckout(false)
|
|
||||||
.call()
|
|
||||||
) { git =>
|
|
||||||
// Get commit Ids to be reverted
|
|
||||||
val commitsToRevert = Using.resource(new RevWalk(git.getRepository)) { revWalk =>
|
|
||||||
pullreq.mergedCommitIds
|
|
||||||
.map(
|
|
||||||
_.split(",")
|
|
||||||
.map { mergedCommitId =>
|
|
||||||
revWalk.parseCommit(git.getRepository.resolve(mergedCommitId))
|
|
||||||
}
|
|
||||||
.toSeq
|
|
||||||
.reverse
|
|
||||||
)
|
|
||||||
.getOrElse(Nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// revert
|
|
||||||
var revert = git.revert
|
|
||||||
commitsToRevert.foreach { id =>
|
|
||||||
revert = revert.include(id)
|
|
||||||
}
|
|
||||||
val newCommit = revert.call()
|
|
||||||
if (newCommit != null) {
|
|
||||||
System.out.println("Reverted commit created: " + newCommit.getName)
|
|
||||||
git.push.call()
|
|
||||||
Some(newCommit.getName)
|
|
||||||
} else {
|
|
||||||
System.out.println("Revert resulted in conflicts.")
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
FileUtil.deleteRecursively(tempDir.toFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
revertCommitName match {
|
|
||||||
case Some(revertCommitName) =>
|
|
||||||
val newIssueId = insertIssue(
|
|
||||||
owner = repository.owner,
|
|
||||||
repository = repository.name,
|
|
||||||
loginUser = loginAccount.userName,
|
|
||||||
title = s"Revert #${issueId}",
|
|
||||||
content = Some(s"Revert #${issueId}"),
|
|
||||||
milestoneId = None,
|
|
||||||
priorityId = None,
|
|
||||||
isPullRequest = true
|
|
||||||
)
|
|
||||||
createPullRequest(
|
|
||||||
originRepository = repository,
|
|
||||||
issueId = newIssueId,
|
|
||||||
originBranch = baseBranch,
|
|
||||||
requestUserName = repository.owner,
|
|
||||||
requestRepositoryName = repository.name,
|
|
||||||
requestBranch = revertBranch,
|
|
||||||
commitIdFrom = baseBranch,
|
|
||||||
commitIdTo = revertCommitName,
|
|
||||||
isDraft = false,
|
|
||||||
loginAccount = loginAccount,
|
|
||||||
settings = context.settings
|
|
||||||
)
|
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/$newIssueId")
|
|
||||||
|
|
||||||
case None =>
|
|
||||||
BadRequest("Failed to create revert commit.")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
case ex: Exception =>
|
|
||||||
ex.printStackTrace()
|
|
||||||
BadRequest(s"Revert failed: ${ex.getMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests whether an logged-in user can manage pull requests.
|
* Tests whether an logged-in user can manage pull requests.
|
||||||
*/
|
*/
|
||||||
@@ -848,4 +740,5 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
case "DISABLE" => false
|
case "DISABLE" => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1076,14 +1076,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
redirect(s"${repository.owner}/${repository.name}/releases")
|
redirect(s"${repository.owner}/${repository.name}/releases")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
|
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
|
||||||
val name = params("name")
|
val name = multiParams("splat").mkString("/")
|
||||||
archiveRepository(name, repository, "")
|
val path = params.get("path").getOrElse("")
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/archive/*/:name")(referrersOnly { repository =>
|
|
||||||
val name = params("name")
|
|
||||||
val path = multiParams("splat").head
|
|
||||||
archiveRepository(name, repository, path)
|
archiveRepository(name, repository, path)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -306,7 +306,8 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
||||||
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
|
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 Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository))
|
||||||
case Left(_) => NotFound()
|
case Left(_) => NotFound()
|
||||||
}
|
}
|
||||||
@@ -316,7 +317,8 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
||||||
val path = multiParams("splat").head
|
val path = multiParams("splat").head
|
||||||
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
|
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 =>
|
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||||
responseRawFile(git, objectId, path, repository)
|
responseRawFile(git, objectId, path, repository)
|
||||||
|
|||||||
@@ -142,10 +142,11 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
|||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||||
revCommit.name
|
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("/")
|
val path = paths.take(paths.size - 1).toList.mkString("/")
|
||||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
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 {
|
fileInfo match {
|
||||||
case Some(f) if !data.sha.contains(f.id.getName) =>
|
case Some(f) if !data.sha.contains(f.id.getName) =>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ trait PullRequestComponent extends TemplateComponent { self: Profile =>
|
|||||||
val commitIdFrom = column[String]("COMMIT_ID_FROM")
|
val commitIdFrom = column[String]("COMMIT_ID_FROM")
|
||||||
val commitIdTo = column[String]("COMMIT_ID_TO")
|
val commitIdTo = column[String]("COMMIT_ID_TO")
|
||||||
val isDraft = column[Boolean]("IS_DRAFT")
|
val isDraft = column[Boolean]("IS_DRAFT")
|
||||||
val mergedCommitIds = column[String]("MERGED_COMMIT_IDS")
|
|
||||||
def * =
|
def * =
|
||||||
(
|
(
|
||||||
userName,
|
userName,
|
||||||
@@ -25,13 +24,12 @@ trait PullRequestComponent extends TemplateComponent { self: Profile =>
|
|||||||
requestBranch,
|
requestBranch,
|
||||||
commitIdFrom,
|
commitIdFrom,
|
||||||
commitIdTo,
|
commitIdTo,
|
||||||
isDraft,
|
isDraft
|
||||||
mergedCommitIds.?
|
|
||||||
).mapTo[PullRequest]
|
).mapTo[PullRequest]
|
||||||
|
|
||||||
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int): Rep[Boolean] =
|
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) =
|
||||||
byIssue(userName, repositoryName, issueId)
|
byIssue(userName, repositoryName, issueId)
|
||||||
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], issueId: Rep[Int]): Rep[Boolean] =
|
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], issueId: Rep[Int]) =
|
||||||
byIssue(userName, repositoryName, issueId)
|
byIssue(userName, repositoryName, issueId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +44,5 @@ case class PullRequest(
|
|||||||
requestBranch: String,
|
requestBranch: String,
|
||||||
commitIdFrom: String,
|
commitIdFrom: String,
|
||||||
commitIdTo: String,
|
commitIdTo: String,
|
||||||
isDraft: Boolean,
|
isDraft: Boolean
|
||||||
mergedCommitIds: Option[String]
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import gitbucket.core.plugin.{PluginRegistry, ReceiveHook}
|
|||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.util.{JGitUtil, LockUtil}
|
import gitbucket.core.util.{JGitUtil, LockUtil}
|
||||||
import gitbucket.core.model.Profile.profile.blockingApi.*
|
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||||
import gitbucket.core.model.activity.{CloseIssueInfo, MergeInfo, PushInfo}
|
import gitbucket.core.model.activity.{CloseIssueInfo, MergeInfo, PushInfo}
|
||||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
import gitbucket.core.service.WebHookService.WebHookPushPayload
|
import gitbucket.core.service.WebHookService.WebHookPushPayload
|
||||||
@@ -19,14 +19,14 @@ import org.eclipse.jgit.errors.NoMergeBaseException
|
|||||||
import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
|
import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
|
||||||
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
|
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
|
||||||
|
|
||||||
import scala.jdk.CollectionConverters.*
|
import scala.jdk.CollectionConverters._
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
|
||||||
trait MergeService {
|
trait MergeService {
|
||||||
self: AccountService & ActivityService & IssuesService & RepositoryService & PullRequestService &
|
self: AccountService & ActivityService & IssuesService & RepositoryService & PullRequestService &
|
||||||
WebHookPullRequestService & WebHookService =>
|
WebHookPullRequestService & WebHookService =>
|
||||||
|
|
||||||
import MergeService.*
|
import MergeService._
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether conflict will be caused in merging within pull request.
|
* Checks whether conflict will be caused in merging within pull request.
|
||||||
@@ -61,16 +61,15 @@ trait MergeService {
|
|||||||
repository: RepositoryInfo,
|
repository: RepositoryInfo,
|
||||||
branch: String,
|
branch: String,
|
||||||
issueId: Int,
|
issueId: Int,
|
||||||
commits: Seq[RevCommit],
|
|
||||||
message: String,
|
message: String,
|
||||||
loginAccount: Account,
|
loginAccount: Account,
|
||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
|
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
|
||||||
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||||
val mergeResult = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
val afterCommitId = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
||||||
.merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName, commits)
|
.merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName)
|
||||||
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
|
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
|
||||||
mergeResult
|
afterCommitId
|
||||||
}
|
}
|
||||||
|
|
||||||
/** rebase to the head of the pull request branch */
|
/** rebase to the head of the pull request branch */
|
||||||
@@ -82,13 +81,13 @@ trait MergeService {
|
|||||||
commits: Seq[RevCommit],
|
commits: Seq[RevCommit],
|
||||||
loginAccount: Account,
|
loginAccount: Account,
|
||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
|
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
|
||||||
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||||
val mergeResult =
|
val afterCommitId =
|
||||||
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
||||||
.rebase(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName, commits)
|
.rebase(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName, commits)
|
||||||
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
|
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
|
||||||
mergeResult
|
afterCommitId
|
||||||
}
|
}
|
||||||
|
|
||||||
/** squash commits in the pull request and append it */
|
/** squash commits in the pull request and append it */
|
||||||
@@ -100,13 +99,13 @@ trait MergeService {
|
|||||||
message: String,
|
message: String,
|
||||||
loginAccount: Account,
|
loginAccount: Account,
|
||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
|
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
|
||||||
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||||
val mergeResult =
|
val afterCommitId =
|
||||||
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
||||||
.squash(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName)
|
.squash(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName)
|
||||||
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
|
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
|
||||||
mergeResult
|
afterCommitId
|
||||||
}
|
}
|
||||||
|
|
||||||
private def callWebHook(
|
private def callWebHook(
|
||||||
@@ -338,7 +337,7 @@ trait MergeService {
|
|||||||
strategy: String,
|
strategy: String,
|
||||||
isDraft: Boolean,
|
isDraft: Boolean,
|
||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(implicit s: Session, c: JsonFormat.Context, context: Context): Either[String, MergeResult] = {
|
)(implicit s: Session, c: JsonFormat.Context, context: Context): Either[String, ObjectId] = {
|
||||||
if (!isDraft) {
|
if (!isDraft) {
|
||||||
if (repository.repository.options.mergeOptions.split(",").contains(strategy)) {
|
if (repository.repository.options.mergeOptions.split(",").contains(strategy)) {
|
||||||
LockUtil.lock(s"${repository.owner}/${repository.name}") {
|
LockUtil.lock(s"${repository.owner}/${repository.name}") {
|
||||||
@@ -494,7 +493,7 @@ trait MergeService {
|
|||||||
commits: Seq[Seq[CommitInfo]],
|
commits: Seq[Seq[CommitInfo]],
|
||||||
receiveHooks: Seq[ReceiveHook],
|
receiveHooks: Seq[ReceiveHook],
|
||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(implicit s: Session, c: JsonFormat.Context): Option[MergeResult] = {
|
)(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = {
|
||||||
val revCommits = Using
|
val revCommits = Using
|
||||||
.resource(new RevWalk(git.getRepository)) { revWalk =>
|
.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||||
commits.flatten.map { commit =>
|
commits.flatten.map { commit =>
|
||||||
@@ -511,7 +510,6 @@ trait MergeService {
|
|||||||
repository,
|
repository,
|
||||||
pullRequest.branch,
|
pullRequest.branch,
|
||||||
issue.issueId,
|
issue.issueId,
|
||||||
revCommits,
|
|
||||||
s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message,
|
s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message,
|
||||||
loginAccount,
|
loginAccount,
|
||||||
settings
|
settings
|
||||||
@@ -602,13 +600,13 @@ object MergeService {
|
|||||||
private val mergedBranchName = s"refs/pull/${issueId}/merge"
|
private val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||||
private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
|
private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
|
||||||
|
|
||||||
lazy val mergeBaseTip: ObjectId = git.getRepository.resolve(s"refs/heads/${branch}")
|
lazy val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||||
lazy val mergeTip: ObjectId = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
lazy val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||||
|
|
||||||
def checkConflictCache(): Option[Option[String]] = {
|
def checkConflictCache(): Option[Option[String]] = {
|
||||||
Option(git.getRepository.resolve(mergedBranchName))
|
Option(git.getRepository.resolve(mergedBranchName))
|
||||||
.flatMap { merged =>
|
.flatMap { merged =>
|
||||||
if (parseCommit(merged).getParents.toSet == Set(mergeBaseTip, mergeTip)) {
|
if (parseCommit(merged).getParents().toSet == Set(mergeBaseTip, mergeTip)) {
|
||||||
// merged branch exists
|
// merged branch exists
|
||||||
Some(None)
|
Some(None)
|
||||||
} else {
|
} else {
|
||||||
@@ -617,7 +615,7 @@ object MergeService {
|
|||||||
}
|
}
|
||||||
.orElse(Option(git.getRepository.resolve(conflictedBranchName)).flatMap { conflicted =>
|
.orElse(Option(git.getRepository.resolve(conflictedBranchName)).flatMap { conflicted =>
|
||||||
val commit = parseCommit(conflicted)
|
val commit = parseCommit(conflicted)
|
||||||
if (commit.getParents.toSet == Set(mergeBaseTip, mergeTip)) {
|
if (commit.getParents().toSet == Set(mergeBaseTip, mergeTip)) {
|
||||||
// conflict branch exists
|
// conflict branch exists
|
||||||
Some(Some(commit.getFullMessage))
|
Some(Some(commit.getFullMessage))
|
||||||
} else {
|
} else {
|
||||||
@@ -653,16 +651,14 @@ object MergeService {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
|
val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
|
||||||
_updateBranch(mergeTipCommit.getTree.getId, message, conflictedBranchName)
|
_updateBranch(mergeTipCommit.getTree().getId(), message, conflictedBranchName)
|
||||||
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
|
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
|
||||||
Some(message)
|
Some(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update branch from cache
|
// update branch from cache
|
||||||
def merge(message: String, committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit
|
def merge(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): ObjectId = {
|
||||||
s: Session
|
|
||||||
): MergeResult = {
|
|
||||||
if (checkConflict().isDefined) {
|
if (checkConflict().isDefined) {
|
||||||
throw new RuntimeException("This pull request can't merge automatically.")
|
throw new RuntimeException("This pull request can't merge automatically.")
|
||||||
}
|
}
|
||||||
@@ -670,7 +666,7 @@ object MergeService {
|
|||||||
throw new RuntimeException(s"Not found branch ${mergedBranchName}")
|
throw new RuntimeException(s"Not found branch ${mergedBranchName}")
|
||||||
})
|
})
|
||||||
// creates merge commit
|
// creates merge commit
|
||||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree.getId, committer, message)
|
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
||||||
|
|
||||||
val refName = s"refs/heads/${branch}"
|
val refName = s"refs/heads/${branch}"
|
||||||
val currentObjectId = git.getRepository.resolve(refName)
|
val currentObjectId = git.getRepository.resolve(refName)
|
||||||
@@ -694,10 +690,10 @@ object MergeService {
|
|||||||
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeResult(objectId, commits.map(_.name()))
|
objectId
|
||||||
}
|
}
|
||||||
|
|
||||||
def rebase(committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit s: Session): MergeResult = {
|
def rebase(committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit s: Session): ObjectId = {
|
||||||
if (checkConflict().isDefined) {
|
if (checkConflict().isDefined) {
|
||||||
throw new RuntimeException("This pull request can't merge automatically.")
|
throw new RuntimeException("This pull request can't merge automatically.")
|
||||||
}
|
}
|
||||||
@@ -717,13 +713,11 @@ object MergeService {
|
|||||||
|
|
||||||
val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip))
|
val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip))
|
||||||
var previousId = mergeBaseTipCommit.getId
|
var previousId = mergeBaseTipCommit.getId
|
||||||
val mergedCommitIds = Seq.newBuilder[String]
|
|
||||||
|
|
||||||
Using.resource(git.getRepository.newObjectInserter) { inserter =>
|
Using.resource(git.getRepository.newObjectInserter) { inserter =>
|
||||||
commits.foreach { commit =>
|
commits.foreach { commit =>
|
||||||
val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId)
|
val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId)
|
||||||
previousId = inserter.insert(nextCommit)
|
previousId = inserter.insert(nextCommit)
|
||||||
mergedCommitIds += previousId.name()
|
|
||||||
}
|
}
|
||||||
inserter.flush()
|
inserter.flush()
|
||||||
}
|
}
|
||||||
@@ -751,10 +745,10 @@ object MergeService {
|
|||||||
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeResult(objectId, mergedCommitIds.result())
|
objectId
|
||||||
}
|
}
|
||||||
|
|
||||||
def squash(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): MergeResult = {
|
def squash(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): ObjectId = {
|
||||||
if (checkConflict().isDefined) {
|
if (checkConflict().isDefined) {
|
||||||
throw new RuntimeException("This pull request can't merge automatically.")
|
throw new RuntimeException("This pull request can't merge automatically.")
|
||||||
}
|
}
|
||||||
@@ -810,7 +804,7 @@ object MergeService {
|
|||||||
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeResult(objectId, Seq(newCommitId.name()))
|
objectId
|
||||||
}
|
}
|
||||||
|
|
||||||
// return treeId
|
// return treeId
|
||||||
@@ -829,5 +823,4 @@ object MergeService {
|
|||||||
mergeResults.asScala.map { case (key, _) => "- `" + key + "`\n" }.mkString
|
mergeResults.asScala.map { case (key, _) => "- `" + key + "`\n" }.mkString
|
||||||
}
|
}
|
||||||
|
|
||||||
case class MergeResult(newCommitId: ObjectId, mergedCommitId: Seq[String])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,15 +63,6 @@ trait PullRequestService {
|
|||||||
.update((baseBranch, commitIdTo))
|
.update((baseBranch, commitIdTo))
|
||||||
}
|
}
|
||||||
|
|
||||||
def updateMergedCommitIds(owner: String, repository: String, issueId: Int, mergedCommitIds: Seq[String])(implicit
|
|
||||||
s: Session
|
|
||||||
): Unit = {
|
|
||||||
PullRequests
|
|
||||||
.filter(_.byPrimaryKey(owner, repository, issueId))
|
|
||||||
.map(pr => pr.mergedCommitIds)
|
|
||||||
.update(mergedCommitIds.mkString(","))
|
|
||||||
}
|
|
||||||
|
|
||||||
def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])(implicit
|
def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])(implicit
|
||||||
s: Session
|
s: Session
|
||||||
): List[PullRequestCount] =
|
): List[PullRequestCount] =
|
||||||
@@ -135,8 +126,7 @@ trait PullRequestService {
|
|||||||
requestBranch,
|
requestBranch,
|
||||||
commitIdFrom,
|
commitIdFrom,
|
||||||
commitIdTo,
|
commitIdTo,
|
||||||
isDraft,
|
isDraft
|
||||||
None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// fetch requested branch
|
// fetch requested branch
|
||||||
@@ -418,10 +408,11 @@ trait PullRequestService {
|
|||||||
.find(x => x.oldPath == file)
|
.find(x => x.oldPath == file)
|
||||||
.map { diff =>
|
.map { diff =>
|
||||||
(diff.oldContent, diff.newContent) match {
|
(diff.oldContent, diff.newContent) match {
|
||||||
case (Some(oldContent), Some(newContent)) =>
|
case (Some(oldContent), Some(newContent)) => {
|
||||||
val oldLines = convertLineSeparator(oldContent, "LF").split("\n")
|
val oldLines = convertLineSeparator(oldContent, "LF").split("\n")
|
||||||
val newLines = convertLineSeparator(newContent, "LF").split("\n")
|
val newLines = convertLineSeparator(newContent, "LF").split("\n")
|
||||||
file -> Option(DiffUtils.diff(oldLines.toList.asJava, newLines.toList.asJava))
|
file -> Option(DiffUtils.diff(oldLines.toList.asJava, newLines.toList.asJava))
|
||||||
|
}
|
||||||
case _ =>
|
case _ =>
|
||||||
file -> None
|
file -> None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ trait WebHookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def validateTargetAddress(settings: SystemSettings, url: String): Boolean = {
|
private def validateTargetAddress(settings: SystemSettings, url: String): Boolean = {
|
||||||
val host = new java.net.URL(url).getHost
|
val host = new java.net.URI(url).toURL.getHost
|
||||||
|
|
||||||
!settings.webHook.blockPrivateAddress ||
|
!settings.webHook.blockPrivateAddress ||
|
||||||
!HttpClientUtil.isPrivateAddress(host) ||
|
!HttpClientUtil.isPrivateAddress(host) ||
|
||||||
|
|||||||
@@ -81,13 +81,4 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@if(issue.closed && pullreq.mergedCommitIds.isDefined){
|
|
||||||
<div class="issue-comment-box" style="margin-bottom: 20px;">
|
|
||||||
<div style="padding: 10px;">
|
|
||||||
<form method="post" action="@helpers.url(repository)/pull/@issue.issueId/revert" style="display:inline;">
|
|
||||||
<button type="submit" class="btn btn-warning pull-right" style="margin-left: 10px;">Revert</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<div class="head" style="height: 24px;">
|
<div class="head" style="height: 24px;">
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<div class="btn-group">
|
<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)/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>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package gitbucket.core
|
package gitbucket.core
|
||||||
|
|
||||||
import java.sql.DriverManager
|
import java.sql.DriverManager
|
||||||
|
|
||||||
import com.dimafeng.testcontainers.{MySQLContainer, PostgreSQLContainer}
|
|
||||||
import io.github.gitbucket.solidbase.Solidbase
|
import io.github.gitbucket.solidbase.Solidbase
|
||||||
import io.github.gitbucket.solidbase.model.Module
|
import io.github.gitbucket.solidbase.model.Module
|
||||||
import liquibase.database.core.{H2Database, MySQLDatabase, PostgresDatabase}
|
import liquibase.database.core.{H2Database, MySQLDatabase, PostgresDatabase}
|
||||||
import org.junit.runner.Description
|
import org.junit.runner.Description
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.Tag
|
import org.scalatest.Tag
|
||||||
|
import org.testcontainers.postgresql.PostgreSQLContainer
|
||||||
|
import org.testcontainers.mysql.MySQLContainer
|
||||||
import org.testcontainers.utility.DockerImageName
|
import org.testcontainers.utility.DockerImageName
|
||||||
|
|
||||||
object ExternalDBTest extends Tag("ExternalDBTest")
|
object ExternalDBTest extends Tag("ExternalDBTest")
|
||||||
@@ -26,24 +26,19 @@ class GitBucketCoreModuleSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
implicit private val suiteDescription: Description = Description.createSuiteDescription(getClass)
|
implicit private val suiteDescription: Description = Description.createSuiteDescription(getClass)
|
||||||
|
|
||||||
Seq("8.0", "5.7").foreach { tag =>
|
Seq("8.4", "5.7").foreach { tag =>
|
||||||
test(s"Migration MySQL $tag", ExternalDBTest) {
|
test(s"Migration MySQL $tag", ExternalDBTest) {
|
||||||
val container = new MySQLContainer() {
|
val container = new MySQLContainer(s"mysql:$tag") {
|
||||||
override val container: org.testcontainers.containers.MySQLContainer[?] =
|
override def getDriverClassName = "org.mariadb.jdbc.Driver"
|
||||||
new org.testcontainers.containers.MySQLContainer(s"mysql:$tag") {
|
override def getJdbcUrl: String = super.getJdbcUrl + "?permitMysqlScheme"
|
||||||
override def getDriverClassName = "org.mariadb.jdbc.Driver"
|
|
||||||
override def getJdbcUrl: String = super.getJdbcUrl + "?permitMysqlScheme"
|
|
||||||
}
|
|
||||||
// TODO https://jira.mariadb.org/browse/CONJ-663
|
|
||||||
container.withCommand("mysqld --default-authentication-plugin=mysql_native_password")
|
|
||||||
}
|
}
|
||||||
container.start()
|
container.start()
|
||||||
try {
|
try {
|
||||||
new Solidbase().migrate(
|
new Solidbase().migrate(
|
||||||
DriverManager.getConnection(
|
DriverManager.getConnection(
|
||||||
container.jdbcUrl,
|
container.getJdbcUrl,
|
||||||
container.username,
|
container.getUsername,
|
||||||
container.password
|
container.getPassword
|
||||||
),
|
),
|
||||||
Thread.currentThread().getContextClassLoader(),
|
Thread.currentThread().getContextClassLoader(),
|
||||||
new MySQLDatabase(),
|
new MySQLDatabase(),
|
||||||
@@ -57,12 +52,12 @@ class GitBucketCoreModuleSpec extends AnyFunSuite {
|
|||||||
|
|
||||||
Seq("11", "10").foreach { tag =>
|
Seq("11", "10").foreach { tag =>
|
||||||
test(s"Migration PostgreSQL $tag", ExternalDBTest) {
|
test(s"Migration PostgreSQL $tag", ExternalDBTest) {
|
||||||
val container = PostgreSQLContainer(DockerImageName.parse(s"postgres:$tag"))
|
val container = new PostgreSQLContainer(DockerImageName.parse(s"postgres:$tag"))
|
||||||
|
|
||||||
container.start()
|
container.start()
|
||||||
try {
|
try {
|
||||||
new Solidbase().migrate(
|
new Solidbase().migrate(
|
||||||
DriverManager.getConnection(container.jdbcUrl, container.username, container.password),
|
DriverManager.getConnection(container.getJdbcUrl, container.getUsername, container.getPassword),
|
||||||
Thread.currentThread().getContextClassLoader(),
|
Thread.currentThread().getContextClassLoader(),
|
||||||
new PostgresDatabase(),
|
new PostgresDatabase(),
|
||||||
new Module(GitBucketCoreModule.getModuleId, GitBucketCoreModule.getVersions)
|
new Module(GitBucketCoreModule.getModuleId, GitBucketCoreModule.getVersions)
|
||||||
|
|||||||
@@ -183,14 +183,14 @@ class ApiIntegrationTest extends AnyFunSuite {
|
|||||||
.branch("main")
|
.branch("main")
|
||||||
.content("create")
|
.content("create")
|
||||||
.message("Create content")
|
.message("Create content")
|
||||||
.path("README.md")
|
.path("test.txt")
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
assert(createResult.getContent.isFile == true)
|
assert(createResult.getContent.isFile)
|
||||||
assert(IOUtils.toString(createResult.getContent.read(), "UTF-8") == "create")
|
assert(IOUtils.toString(createResult.getContent.read(), "UTF-8") == "create")
|
||||||
|
|
||||||
val content1 = repo.getFileContent("README.md")
|
val content1 = repo.getFileContent("test.txt")
|
||||||
assert(content1.isFile == true)
|
assert(content1.isFile)
|
||||||
assert(IOUtils.toString(content1.read(), "UTF-8") == "create")
|
assert(IOUtils.toString(content1.read(), "UTF-8") == "create")
|
||||||
assert(content1.getSha == createResult.getContent.getSha)
|
assert(content1.getSha == createResult.getContent.getSha)
|
||||||
|
|
||||||
@@ -200,14 +200,14 @@ class ApiIntegrationTest extends AnyFunSuite {
|
|||||||
.branch("main")
|
.branch("main")
|
||||||
.content("update")
|
.content("update")
|
||||||
.message("Update content")
|
.message("Update content")
|
||||||
.path("README.md")
|
.path("test.txt")
|
||||||
.sha(content1.getSha)
|
.sha(content1.getSha)
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
assert(updateResult.getContent.isFile == true)
|
assert(updateResult.getContent.isFile)
|
||||||
assert(IOUtils.toString(updateResult.getContent.read(), "UTF-8") == "update")
|
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(content2.isFile == true)
|
||||||
assert(IOUtils.toString(content2.read(), "UTF-8") == "update")
|
assert(IOUtils.toString(content2.read(), "UTF-8") == "update")
|
||||||
assert(content2.getSha == updateResult.getContent.getSha)
|
assert(content2.getSha == updateResult.getContent.getSha)
|
||||||
|
|||||||
@@ -154,8 +154,7 @@ object ApiSpecModels {
|
|||||||
requestBranch = "new-topic",
|
requestBranch = "new-topic",
|
||||||
commitIdFrom = sha1,
|
commitIdFrom = sha1,
|
||||||
commitIdTo = sha1,
|
commitIdTo = sha1,
|
||||||
isDraft = true,
|
isDraft = true
|
||||||
mergedCommitIds = None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val commitComment: CommitComment = CommitComment(
|
val commitComment: CommitComment = CommitComment(
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ class MergeServiceSpec extends AnyFunSpec with ServiceSpecBase {
|
|||||||
repository,
|
repository,
|
||||||
branch,
|
branch,
|
||||||
issueId,
|
issueId,
|
||||||
Seq(git.getRepository.parseCommit(commitId)),
|
|
||||||
"merged",
|
"merged",
|
||||||
context.loginAccount.get,
|
context.loginAccount.get,
|
||||||
context.settings
|
context.settings
|
||||||
|
|||||||
Reference in New Issue
Block a user