Compare commits

..

1 Commits

Author SHA1 Message Date
takezoe
753403b4c8 Implement /meta API endpoint 2022-10-30 21:02:23 +09:00
205 changed files with 5083 additions and 6498 deletions

View File

@@ -1,8 +0,0 @@
# update scalafmt
3d5ca44d66c77a46770a65a895c9737c542690f6
# Scala Steward: Reformat with scalafmt 3.8.2
f1360f44c61f8e12666965c10e79f11cd75d6d30
# Scala Steward: Reformat with scalafmt 3.9.7
a54fb4960ff0762738f4895cdc29bf2715a57f87

1
.github/CODEOWNERS vendored
View File

@@ -2,4 +2,3 @@
build.sbt @xuwei-k build.sbt @xuwei-k
project/* @xuwei-k project/* @xuwei-k
.github/workflows/* @xuwei-k

18
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,18 @@
### Before submitting an issue to GitBucket I have first:
- [ ] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [ ] searched for similar already existing issue
- [ ] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
*(if you have performed all the above, remove the paragraph and continue describing the issue with template below)*
## Issue
**Impacted version**: xxxx
**Deployment mode**: *explain here how you use GitBucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
**Problem description**:
- *be as explicit as you can*
- *describe the problem and its symptoms*
- *explain how to reproduce*
- *attach whatever information that can help understanding the context (screen capture, log files)*

View File

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

View File

@@ -1,41 +0,0 @@
name: Report issue
description: Report a problem or feature request with GitBucket
body:
- type: markdown
attributes:
value: |
### Before submitting an issue to GitBucket, please ensure you have:
- 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
attributes:
label: Prerequisites
options:
- label: I have read the contribution guidelines
- label: I have searched for similar issues
- label: I have read the documentation and wiki
- type: input
id: impacted_version
attributes:
label: Impacted version
description: Which version of GitBucket is affected?
placeholder: e.g. 4.37.0
- type: input
id: deployment_mode
attributes:
label: Deployment mode
description: How do you use GitBucket? (standalone app, under webcontainer, with an HTTP frontend, etc.)
placeholder: e.g. Standalone app, Tomcat, nginx
- type: textarea
id: problem_description
attributes:
label: Problem description
description: Be as explicit as you can. Describe the problem, its symptoms, how to reproduce, and attach any relevant information (screenshots, logs, etc.)
placeholder: Describe the problem and how to reproduce it

View File

@@ -7,13 +7,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
fail-fast: false
matrix: matrix:
java: [17, 21] java: [8, 11, 17]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Cache - name: Cache
uses: actions/cache@v4 uses: actions/cache@v3
env: env:
cache-name: cache-sbt-libs cache-name: cache-sbt-libs
with: with:
@@ -23,20 +22,18 @@ 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@v3
with: with:
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
distribution: adopt distribution: adopt
- name: Setup sbt launcher
uses: sbt/setup-sbt@v1
- name: Run tests - name: Run tests
run: sbt scalafmtSbtCheck scalafmtCheckAll test run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
- name: Scala 3 - name: Scala 3
run: sbt '++ 3.x' update # TODO run: sbt '++ 3.1.2!' update # TODO
- 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@v3
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
.gitignore vendored
View File

@@ -4,7 +4,6 @@
.ensime_cache .ensime_cache
.DS_Store .DS_Store
.java-version .java-version
.tmp
# sbt specific # sbt specific
dist/* dist/*

View File

@@ -3,6 +3,7 @@ updates.limit = 3
updates.includeScala = true updates.includeScala = true
updates.pin = [ updates.pin = [
{ groupId = "org.eclipse.jetty", version = "10." } { groupId = "org.eclipse.jetty", version = "9." }
{ groupId = "org.mariadb.jdbc", version = "2." } { groupId = "org.eclipse.jgit", version = "5." }
{ groupId = "com.zaxxer", version = "4." }
] ]

View File

@@ -1,24 +1,12 @@
version = "3.9.9" version = "1.5.1"
project.git = true project.git = true
maxColumn = 120 maxColumn = 120
docstrings.style = keep docstrings = JavaDoc
align.tokens = ["%", "%%", {code = "=>", owner = "Case"}] align.tokens = ["%", "%%", {code = "=>", owner = "Case"}]
align.openParenCallSite = false align.openParenCallSite = false
align.openParenDefnSite = false align.openParenDefnSite = false
continuationIndent.callSite = 2 continuationIndent.callSite = 2
continuationIndent.defnSite = 2 continuationIndent.defnSite = 2
danglingParentheses.preset = true danglingParentheses = true
runner.dialect = scala213source3
rewrite.trailingCommas.style = keep
fileOverride {
"glob:**/*.sbt" {
runner.dialect = scala212
rewrite.scala3.convertToNewSyntax = false
}
}
rewrite.scala3.convertToNewSyntax = true
runner.dialectOverride.allowSignificantIndentation = false
runner.dialectOverride.allowAsForImportRename = false
runner.dialectOverride.allowStarWildcardImport = false

View File

@@ -1,66 +1,6 @@
# 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.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
# Recreate database using the new version of H2
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.jar
$ 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
## 4.42.0 - 30 Dec 2024
- Increase max branch name length 100 -> 255
- Fix some GitHub incompatible Web APIs
- Apply user-defined CSS after all plugins
- Improve performance of listing commit logs
- Drop Java 11 support. Java 17 is now required
## 4.41.0 - 18 May 2024
- Simplify pull request UI
- Keyword search for issues and pull requests
- New settings for max files and lines limit in showing diff
- Adjust the default branch automatically when cloning external repository
- Fix layout of branch selector
- Performance improvement for listing branches
- Upgrade internal libraries
## 4.40.0 - 22 Oct 2023
- Configurable default branch name
- Support custom fields of issues and pull requests in search condition
- Create pull request from default branch of forked repositories
- News feed shows activities of all visible repositories
- Drop Java 8 support
- Improve git push performance
## 4.39.0 - 29 Apr 2023
- Support enum type in custom fields of Issues and Pull requests
- Hide large diffs by default
- Add new options to make it possible to run GitBucket using multiple machines
- Fix many API issues
## 4.38.4 - 2 Nov 2022
- Downgrade MariaDB JDBC drive to avoid unknown error
## 4.38.3 - 30 Oct 2022 ## 4.38.3 - 30 Oct 2022
- Fix several issues around multiple assignees in issues and pull requests - Fix several issues around multiple assignees in issues and pull requests
- Fix IllegalStateException when returning unknown avatar image - Fix IllegalStateException when returning unknown avatar image

View File

@@ -1,4 +1,4 @@
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.svg)](https://gitter.im/gitbucket/gitbucket) [![build](https://github.com/gitbucket/gitbucket/actions/workflows/build.yml/badge.svg)](https://github.com/gitbucket/gitbucket/actions/workflows/build.yml) [![gitbucket Scala version support](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket/latest-by-scala-version.svg)](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/gitbucket/gitbucket/blob/master/LICENSE) GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.svg)](https://gitter.im/gitbucket/gitbucket) [![build](https://github.com/gitbucket/gitbucket/workflows/build/badge.svg?branch=master)](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [![gitbucket Scala version support](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket/latest-by-scala-version.svg)](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/gitbucket/gitbucket/blob/master/LICENSE)
========= =========
GitBucket is a Git web platform powered by Scala offering: GitBucket is a Git web platform powered by Scala offering:
@@ -24,7 +24,7 @@ The current version of GitBucket provides many features such as:
Installation Installation
-------- --------
GitBucket requires **Java 17**. You have to install it, if it is not already installed. GitBucket requires **Java8**. You have to install it, if it is not already installed.
1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`. 1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**. 2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
@@ -56,33 +56,32 @@ 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 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 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 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.38.x
------------- -------------
## 4.43.0 - 29 Jun 2025 ## 4.38.3 - 30 Oct 2022
- Upgrade H2 database from 1.x to 2.x - Fix several issues around multiple assignees in issues and pull requests
- Fix IllegalStateException when returning unknown avatar image
Note that upgrading from h2 1.x to 2.x requires data file migration: https://www.h2database.com/html/migration-to-v2.html ## 4.38.2 - 20 Sep 2022
- Resurrect assignee icons on the issue list
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: ## 4.38.1 - 10 Sep 2022
```bash - Fix comment diff in Chrome 105
# Export database using the current version of H2 - Fix Markdown table CSS
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar - Fix HTML rendering of multiple asignees
$ java -cp h2-1.4.199.jar org.h2.tools.Script -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
# Recreate database using the new version of H2 ## 4.38.0 - 3 Sep 2022
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.jar - Support multiple assignees for Issues and Pull requests
$ java -cp h2-2.3.232.jar org.h2.tools.RunScript -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql - Custom fields for issues and pull requests
``` - Reset password by users
- Allow to configure Jetty idle timeout in standalone mode
- Horizontal scroll for too wide tables in Markdown
- Hide header content on signin and register page
- Fix the default charset of the online editor in the repository viewer
- Fix the milestone count
- Some improvements and bugfixes for WebAPI and WebHook
In addition, if `~/.gitbucket/database.conf` has the following configuration, remove `;MVCC=true` from `url`. See the [change log](CHANGELOG.md) for all of the updates.
```
db {
url = "jdbc:h2:${DatabaseHome};MVCC=true" // => "jdbc:h2:${DatabaseHome}"
...
}
```
See the [change log](CHANGELOG.md) for all the past updates.

142
build.sbt
View File

@@ -1,76 +1,85 @@
import com.typesafe.sbt.license.{DepModuleInfo, LicenseInfo}
import com.jsuereth.sbtpgp.PgpKeys._ 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.38.3"
val ScalatraVersion = "3.1.2" val ScalatraVersion = "2.8.4"
val JettyVersion = "10.0.25" val JettyVersion = "9.4.49.v20220914"
val JgitVersion = "6.10.1.202505221210-r" val JgitVersion = "5.13.1.202206130422-r"
lazy val root = (project in file(".")) lazy val root = (project in file("."))
.enablePlugins(SbtTwirl, ContainerPlugin) .enablePlugins(SbtTwirl, ScalatraPlugin)
sourcesInBase := false sourcesInBase := false
organization := Organization organization := Organization
name := Name name := Name
version := GitBucketVersion version := GitBucketVersion
scalaVersion := "2.13.16" scalaVersion := "2.13.10"
crossScalaVersions += "3.7.2"
// scalafmtOnCompile := true // scalafmtOnCompile := true
coverageExcludedPackages := ".*\\.html\\..*" coverageExcludedPackages := ".*\\.html\\..*"
// dependency settings
resolvers ++= Seq(
Classpaths.typesafeReleases,
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
)
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % JgitVersion, "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % JgitVersion,
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % JgitVersion, "org.eclipse.jgit" % "org.eclipse.jgit.archive" % JgitVersion,
"org.scalatra" %% "scalatra-javax" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-json-javax" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-forms-javax" % ScalatraVersion, "org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.1.0-M8", "org.json4s" %% "json4s-jackson" % "4.0.6" cross CrossVersion.for3Use2_13,
"commons-io" % "commons-io" % "2.20.0", "commons-io" % "commons-io" % "2.11.0",
"io.github.gitbucket" % "solidbase" % "1.1.0", "io.github.gitbucket" % "solidbase" % "1.0.5",
"io.github.gitbucket" % "markedj" % "1.0.20", "io.github.gitbucket" % "markedj" % "1.0.17",
"org.tukaani" % "xz" % "1.10", "org.apache.commons" % "commons-compress" % "1.21",
"org.apache.commons" % "commons-compress" % "1.28.0", "org.apache.commons" % "commons-email" % "1.5",
"org.apache.commons" % "commons-email" % "1.6.0", "commons-net" % "commons-net" % "3.8.0",
"commons-net" % "commons-net" % "3.12.0", "org.apache.httpcomponents" % "httpclient" % "4.5.13",
"org.apache.httpcomponents" % "httpclient" % "4.5.14", "org.apache.sshd" % "apache-sshd" % "2.9.1" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.sshd" % "apache-sshd" % "2.15.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ( "org.apache.tika" % "tika-core" % "2.5.0",
"org.apache.sshd", "com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
"sshd-mina"
) exclude ("org.apache.sshd", "sshd-netty")
exclude ("org.apache.sshd", "sshd-spring-sftp"),
"org.apache.tika" % "tika-core" % "3.2.2",
"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" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.12", "org.mariadb.jdbc" % "mariadb-java-client" % "3.0.8",
"org.postgresql" % "postgresql" % "42.7.7", "org.postgresql" % "postgresql" % "42.5.0",
"ch.qos.logback" % "logback-classic" % "1.5.18", "ch.qos.logback" % "logback-classic" % "1.2.11",
"com.zaxxer" % "HikariCP" % "7.0.1" exclude ("org.slf4j", "slf4j-api"), "com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.4", "com.typesafe" % "config" % "1.4.2",
"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.12",
"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.18",
"com.github.zafarkhaja" % "java-semver" % "0.10.2", "com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "11.27", "com.nimbusds" % "oauth2-oidc-sdk" % "10.1",
"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" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
"org.mockito" % "mockito-core" % "5.18.0" % "test", "org.mockito" % "mockito-core" % "4.8.1" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.43.0" % "test", "com.dimafeng" %% "testcontainers-scala" % "0.40.11" % "test",
"org.testcontainers" % "mysql" % "1.21.3" % "test", "org.testcontainers" % "mysql" % "1.17.5" % "test",
"org.testcontainers" % "postgresql" % "1.21.3" % "test", "org.testcontainers" % "postgresql" % "1.17.5" % "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" % "0.3.0",
"org.kohsuke" % "github-api" % "1.329" % "test" "org.kohsuke" % "github-api" % "1.313" % "test"
) )
libraryDependencies ~= {
_.map {
case x if x.name == "twirl-api" =>
x cross CrossVersion.for3Use2_13
case x =>
x
}
}
// Compiler settings // Compiler settings
scalacOptions := Seq( scalacOptions := Seq(
"-deprecation", "-deprecation",
@@ -80,16 +89,8 @@ scalacOptions := Seq(
"-Wunused:imports", "-Wunused:imports",
"-Wconf:cat=unused&src=twirl/.*:s,cat=unused&src=scala/gitbucket/core/model/[^/]+\\.scala:s" "-Wconf:cat=unused&src=twirl/.*:s,cat=unused&src=scala/gitbucket/core/model/[^/]+\\.scala:s"
) )
scalacOptions ++= { compile / javacOptions ++= Seq("-target", "8", "-source", "8")
scalaBinaryVersion.value match { Jetty / javaOptions += "-Dlogback.configurationFile=/logback-dev.xml"
case "2.13" =>
Seq("-Xsource:3-cross")
case _ =>
Nil
}
}
compile / javacOptions ++= Seq("-target", "11", "-source", "11")
Container / javaOptions += "-Dlogback.configurationFile=/logback-dev.xml"
// Test settings // Test settings
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest") //testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
@@ -113,8 +114,8 @@ assembly / assemblyMergeStrategy := {
// Exclude a war file from published artifacts // Exclude a war file from published artifacts
signedArtifacts := { signedArtifacts := {
signedArtifacts.value.filterNot { case (_, file) => signedArtifacts.value.filterNot {
file.getName.endsWith(".war") || file.getName.endsWith(".war.asc") case (_, file) => file.getName.endsWith(".war") || file.getName.endsWith(".war.asc")
} }
} }
@@ -124,6 +125,7 @@ Keys.ivyConfigurations += ExecutableConfig
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
@@ -156,11 +158,9 @@ executableKey := {
// include jetty classes // include jetty classes
val jettyJars = Keys.update.value select configurationFilter(name = ExecutableConfig.name) val jettyJars = Keys.update.value select configurationFilter(name = ExecutableConfig.name)
jettyJars foreach { jar => jettyJars foreach { jar =>
IO unzip ( IO unzip (jar, temp, (name: String) =>
jar, (name startsWith "javax/") ||
temp, (name startsWith "org/"))
(name: String) => (name startsWith "javax/") || (name startsWith "org/") || (name startsWith "META-INF/services/")
)
} }
// include original war file // include original war file
@@ -185,7 +185,7 @@ executableKey := {
val url = "https://github.com/" + val url = "https://github.com/" +
s"gitbucket/gitbucket-${pluginId}-plugin/releases/download/${pluginVersion}/gitbucket-${pluginId}-plugin-${pluginVersion}.jar" s"gitbucket/gitbucket-${pluginId}-plugin/releases/download/${pluginVersion}/gitbucket-${pluginId}-plugin-${pluginVersion}.jar"
log info s"Download: ${url}" log info s"Download: ${url}"
IO transfer (new java.net.URI(url).toURL.openStream, pluginsDir / url.substring(url.lastIndexOf("/") + 1)) IO transfer (new java.net.URL(url).openStream, pluginsDir / url.substring(url.lastIndexOf("/") + 1))
case _ => () case _ => ()
} }
} }
@@ -206,7 +206,8 @@ executableKey := {
"md5" -> "MD5", "md5" -> "MD5",
"sha1" -> "SHA-1", "sha1" -> "SHA-1",
"sha256" -> "SHA-256" "sha256" -> "SHA-256"
).foreach { case (extension, algorithm) => ).foreach {
case (extension, algorithm) =>
val checksumFile = workDir / (warName + "." + extension) val checksumFile = workDir / (warName + "." + extension)
Checksums generate (outputFile, checksumFile, algorithm) Checksums generate (outputFile, checksumFile, algorithm)
} }
@@ -216,9 +217,9 @@ executableKey := {
outputFile outputFile
} }
publishTo := { publishTo := {
val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
else localStaging.value else Some("releases" at nexus + "service/local/staging/deploy/maven2")
} }
publishMavenStyle := true publishMavenStyle := true
pomIncludeRepository := { _ => pomIncludeRepository := { _ =>
@@ -284,12 +285,7 @@ Test / testOptions ++= {
} }
} }
Container / javaOptions ++= Seq( Jetty / javaOptions ++= Seq(
"-Dlogback.configurationFile=/logback-dev.xml",
"-Xdebug", "-Xdebug",
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
"-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF",
// "-Ddev-features=keep-session"
) )
Container / containerLibs := Seq(("org.eclipse.jetty" % "jetty-runner" % JettyVersion).intransitive())
Container / containerMain := "org.eclipse.jetty.runner.Runner"

View File

@@ -1,7 +1,7 @@
How to build and run from the source tree How to build and run from the source tree
======== ========
First of all, Install [sbt](https://www.scala-sbt.org/index.html). First of all, Install [sbt](http://www.scala-sbt.org/index.html).
```shell ```shell
$ brew install sbt $ brew install sbt
@@ -13,26 +13,12 @@ Run for Development
If you want to test GitBucket, type the following command in the root directory of the source tree. If you want to test GitBucket, type the following command in the root directory of the source tree.
```shell ```shell
$ sbt ~container:start $ sbt ~jetty:start
``` ```
Then access `http://localhost:8080/` in your browser. The default administrator account is `root` and password is `root`. Then access `http://localhost:8080/` in your browser. The default administrator account is `root` and password is `root`.
Source code modifications are detected and a reloading happens automatically. Source code modifications are detected and a reloading happens automatically. You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
Note that HttpSession is cleared when auto-reloading happened.
This is a bit annoying when developing features that requires sign-in.
You can keep HttpSession even if GitBucket is restarted by enabling this configuration in `build.sbt`:
https://github.com/gitbucket/gitbucket/blob/3dcc0aee3c4413b05be7c03476626cb202674afc/build.sbt#L292
Or by launching GitBucket with the following command:
```shell
sbt '; set Container/javaOptions += "-Ddev-features=keep-session" ; ~container:start'
```
Note that this feature serializes HttpSession on the local disk and assigns all requests to the same session
which means you cannot test multi users behavior in this mode.
Build war file Build war file
-------- --------
@@ -51,8 +37,7 @@ To build an executable war file, run
$ sbt executable $ sbt executable
``` ```
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
We release this war file as release artifact.
Run tests spec Run tests spec
--------- ---------

View File

@@ -1,6 +1,6 @@
Debug GitBucket on IntelliJ Debug GitBucket on IntelliJ
======== ========
Add following configuration for allowing remote debugging to `build.sbt`: Add following configuration for allowing remote debugging to `buils.sbt`:
```scala ```scala
javaOptions in Jetty ++= Seq( javaOptions in Jetty ++= Seq(
@@ -12,7 +12,7 @@ javaOptions in Jetty ++= Seq(
Run GitBucket: Run GitBucket:
```shell ```shell
$ sbt ~container:start $ sbt ~jetty:start
``` ```
In IntelliJ, create remote debug configuration as follows. Make sure port number is same as above configuration. In IntelliJ, create remote debug configuration as follows. Make sure port number is same as above configuration.

View File

@@ -5,17 +5,13 @@ GitBucket persists all data into __HOME/.gitbucket__ in default (In 1.9 or befor
This directory has following structure: This directory has following structure:
``` ```
* /HOME/.gitbucket * /HOME/gitbucket
* gitbucket.conf
* database.conf
* activity.log
* data.mv.db, data.trace.db (H2 data files if the default embed H2 is used)
* /repositories * /repositories
* /USER_NAME * /USER_NAME
* /REPO_NAME.git (substance of repository. GitServlet sees this directory) * /REPO_NAME.git (substance of repository. GitServlet sees this directory)
* /REPO_NAME.wiki.git (wiki repository) * /REPO_NAME.wiki.git (wiki repository)
* /REPO_NAME * /REPO_NAME
* /issues (files attached to issue) * /issues (files which are attached to issue)
* /lfs (LFS managed files) * /lfs (LFS managed files)
* /data * /data
* /USER_NAME * /USER_NAME
@@ -24,8 +20,6 @@ This directory has following structure:
* /plugins * /plugins
* plugin.jar * plugin.jar
* /.installed (copied available plugins from the parent directory automatically) * /.installed (copied available plugins from the parent directory automatically)
* /sessions
* HTTP session data created (if '--save_sessions' option is used in the standalone mode)
* /tmp * /tmp
* /_upload * /_upload
* /SESSION_ID (removed at session timeout) * /SESSION_ID (removed at session timeout)

View File

@@ -38,26 +38,15 @@ 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, hit following command to publish artifacts to the sonatype OSS repository:
```bash ```bash
$ sbt publishSigned $ sbt publishSigned
``` ```
Next, upload artifacts to Sonatype's Central Portal with the following command: Then logged-in to https://oss.sonatype.org/, close and release the repository.
```bash 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.
$ 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 ### Make release war file
@@ -66,4 +55,5 @@ Run `sbt executable`. The release war file and fingerprint are generated into `t
```bash ```bash
$ sbt executable $ sbt executable
``` ```
Create new release from the corresponded tag on GitHub, then upload generated jar file and fingerprints to the release. Create new release from the corresponded tag on GitHub, then upload generated jar file and fingerprints to the release.

View File

@@ -1,6 +1,6 @@
Mapping and Validation Mapping and Validation
======== ========
GitBucket uses [scalatra-forms](https://scalatra.org/guides/2.8/formats/forms.html) to validate request parameters and map them to the scala object. This is inspired by Play2 form mapping / validation. GitBucket uses [scalatra-forms](http://scalatra.org/guides/2.6/formats/forms.html) to validate request parameters and map them to the scala object. This is inspired by Play2 form mapping / validation.
At first, define the mapping as following: At first, define the mapping as following:

View File

@@ -1 +1 @@
sbt.version=1.11.4 sbt.version=1.7.2

View File

@@ -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.4.6")
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.9") addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.0.0")
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.1.2")
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.7.0") addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.0")
addDependencyTreePlugin addDependencyTreePlugin

View File

@@ -65,15 +65,9 @@ public class JettyLauncher {
boolean saveSessions = false; boolean saveSessions = false;
for(String arg: args) { for(String arg: args) {
if (arg.equals("--save_sessions")) { if(arg.equals("--save_sessions")) {
saveSessions = true; saveSessions = true;
} }
if (arg.equals("--disable_news_feed")) {
System.setProperty("gitbucket.disableNewsFeed", "true");
}
if (arg.equals("--disable_cache")) {
System.setProperty("gitbucket.disableCache", "true");
}
if(arg.startsWith("--") && arg.contains("=")) { if(arg.startsWith("--") && arg.contains("=")) {
String[] dim = arg.split("=", 2); String[] dim = arg.split("=", 2);
if(dim.length == 2) { if(dim.length == 2) {
@@ -155,7 +149,7 @@ public class JettyLauncher {
} }
if (connectorsSet.contains(Connectors.HTTPS)) { if (connectorsSet.contains(Connectors.HTTPS)) {
final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); final SslContextFactory sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(requireNonNull(keyStorePath, sslContextFactory.setKeyStorePath(requireNonNull(keyStorePath,
"You must specify a path to an SSL keystore via the --key_store_path command line argument" + "You must specify a path to an SSL keystore via the --key_store_path command line argument" +

View File

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

View File

@@ -7,7 +7,7 @@
</appender> </appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender"> <appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>.tmp/gitbucket.log</file> <file>gitbucket.log</file>
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>

View File

@@ -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"/>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="CUSTOM_FIELD">
<column name="CONSTRAINTS" type="varchar(200)" nullable="true"/>
</addColumn>
</changeSet>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<dropForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT"/>
<dropPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT"/>
<dropForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_FK0" baseTableName="PROTECTED_BRANCH"/>
<dropPrimaryKey constraintName="IDX_PROTECTED_BRANCH_PK" tableName="PROTECTED_BRANCH"/>
<modifyDataType columnName="DEFAULT_BRANCH" newDataType="varchar(255)" tableName="REPOSITORY"/>
<modifyDataType columnName="BRANCH" newDataType="varchar(255)" tableName="PULL_REQUEST"/>
<modifyDataType columnName="REQUEST_BRANCH" newDataType="varchar(255)" tableName="PULL_REQUEST"/>
<modifyDataType columnName="BRANCH" newDataType="varchar(255)" tableName="PROTECTED_BRANCH"/>
<modifyDataType columnName="BRANCH" newDataType="varchar(255)" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT"/>
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_PK" tableName="PROTECTED_BRANCH" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_FK0" baseTableName="PROTECTED_BRANCH" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
</changeSet>

View File

@@ -1,40 +0,0 @@
<?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"/>
<!--================================================================================================-->
<!-- PULL_REQUEST -->
<!--================================================================================================-->
<addColumn tableName="PULL_REQUEST">
<column name="MERGED_COMMIT_IDS" type="text" nullable="true"/>
</addColumn>
</changeSet>

View File

@@ -4,6 +4,7 @@ import java.io.FileOutputStream
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.sql.Connection import java.sql.Connection
import java.util.UUID import java.util.UUID
import gitbucket.core.model.Activity import gitbucket.core.model.Activity
import gitbucket.core.util.Directory.ActivityLog import gitbucket.core.util.Directory.ActivityLog
import gitbucket.core.util.JDBCUtil import gitbucket.core.util.JDBCUtil
@@ -14,7 +15,6 @@ import org.json4s.{Formats, NoTypeHints}
import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write import org.json4s.jackson.Serialization.write
import java.util.logging.Level
import scala.util.Using import scala.util.Using
object GitBucketCoreModule object GitBucketCoreModule
@@ -76,7 +76,8 @@ object GitBucketCoreModule
import JDBCUtil._ import JDBCUtil._
val conn = context.get(Solidbase.CONNECTION).asInstanceOf[Connection] val conn = context.get(Solidbase.CONNECTION).asInstanceOf[Connection]
val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") { rs => val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") {
rs =>
Activity( Activity(
activityId = UUID.randomUUID().toString, activityId = UUID.randomUUID().toString,
userName = rs.getString("USER_NAME"), userName = rs.getString("USER_NAME"),
@@ -113,15 +114,5 @@ object GitBucketCoreModule
new Version("4.38.0", new LiquibaseMigration("update/gitbucket-core_4.38.xml")), new Version("4.38.0", new LiquibaseMigration("update/gitbucket-core_4.38.xml")),
new Version("4.38.1"), new Version("4.38.1"),
new Version("4.38.2"), new Version("4.38.2"),
new Version("4.38.3"), new Version("4.38.3")
new Version("4.38.4"), )
new Version("4.39.0", new LiquibaseMigration("update/gitbucket-core_4.39.xml")),
new Version("4.40.0"),
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.44.0", new LiquibaseMigration("update/gitbucket-core_4.44.xml"))
) {
java.util.logging.Logger.getLogger("liquibase").setLevel(Level.SEVERE)
}

View File

@@ -1,6 +0,0 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue
*/
case class AddLabelsToAnIssue(labels: Seq[String])

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/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection * https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/ */
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtectionResponse)( case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtection)(
repositoryName: RepositoryName repositoryName: RepositoryName
) extends FieldSerializable { ) extends FieldSerializable {
val _links = val _links =

View File

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

View File

@@ -1,21 +0,0 @@
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

@@ -14,8 +14,7 @@ case class ApiComment(id: Int, user: ApiUser, body: String, created_at: Date, up
isPullRequest: Boolean isPullRequest: Boolean
) { ) {
val html_url = ApiPath( val html_url = ApiPath(
s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${issueId}#comment-${id}"
else { "issues" }}/${issueId}#comment-${id}"
) )
} }

View File

@@ -29,7 +29,7 @@ case class ApiCommit(
object ApiCommit { object ApiCommit {
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = { def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = {
val diffs = JGitUtil.getDiffs(git = git, from = None, to = commit.id, fetchContent = false, makePatch = false) val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false)
ApiCommit( ApiCommit(
id = commit.id, id = commit.id,
message = commit.fullMessage, message = commit.fullMessage,

View File

@@ -69,8 +69,7 @@ object ApiCommits {
} }
File( File(
filename = if (diff.changeType == ChangeType.DELETE) { diff.oldPath } filename = if (diff.changeType == ChangeType.DELETE) { diff.oldPath } else { diff.newPath },
else { diff.newPath },
additions = additions, additions = additions,
deletions = deletions, deletions = deletions,
changes = additions + deletions, changes = additions + deletions,
@@ -107,9 +106,7 @@ object ApiCommits {
message = commitInfo.shortMessage, message = commitInfo.shortMessage,
comment_count = commentCount, comment_count = commentCount,
tree = Tree( tree = Tree(
url = ApiPath( url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${commitInfo.id}"), // TODO This endpoint has not been implemented yet.
s"/api/v3/repos/${repositoryName.fullName}/tree/${commitInfo.id}"
), // TODO This endpoint has not been implemented yet.
sha = commitInfo.id sha = commitInfo.id
) )
), ),
@@ -117,9 +114,7 @@ object ApiCommits {
committer = ApiUser(committer), committer = ApiUser(committer),
parents = commitInfo.parents.map { parent => parents = commitInfo.parents.map { parent =>
Tree( Tree(
url = ApiPath( url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${parent}"), // TODO This endpoint has not been implemented yet.
s"/api/v3/repos/${repositoryName.fullName}/tree/${parent}"
), // TODO This endpoint has not been implemented yet.
sha = parent sha = parent
) )
}, },

View File

@@ -22,7 +22,8 @@ object ApiContents {
ApiContents("dir", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName) ApiContents("dir", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName)
} else { } else {
content content
.map(arr => .map(
arr =>
ApiContents( ApiContents(
"file", "file",
fileInfo.name, fileInfo.name,

View File

@@ -23,8 +23,7 @@ case class ApiIssue(
val id = 0 // dummy id val id = 0 // dummy id
val assignee = assignees.headOption val assignee = assignees.headOption
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments") val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}")
else { "issues" }}/${number}")
val pull_request = if (isPullRequest) { val pull_request = if (isPullRequest) {
Some( Some(
Map( Map(
@@ -55,8 +54,7 @@ object ApiIssue {
assignees = assignees, assignees = assignees,
labels = labels, labels = labels,
milestone = milestone, milestone = milestone,
state = if (issue.closed) { "closed" } state = if (issue.closed) { "closed" } else { "open" },
else { "open" },
body = issue.content.getOrElse(""), body = issue.content.getOrElse(""),
created_at = issue.registeredDate, created_at = issue.registeredDate,
updated_at = issue.updatedDate updated_at = issue.updatedDate

View File

@@ -27,10 +27,10 @@ case class ApiPullRequest(
val id = 0 // dummy id val id = 0 // dummy id
val assignee = assignees.headOption val assignee = assignees.headOption
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}") val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
// val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff") //val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
// val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch") //val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
val url = ApiPath(s"${base.repo.url.path}/pulls/${number}") val url = ApiPath(s"${base.repo.url.path}/pulls/${number}")
// val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}") //val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}")
val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits") val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits")
val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments") val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments")
val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}") val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}")
@@ -69,8 +69,7 @@ object ApiPullRequest {
) )
case class Commit(sha: String, ref: String, repo: ApiRepository)(baseOwner: String) { case class Commit(sha: String, ref: String, repo: ApiRepository)(baseOwner: String) {
val label = if (baseOwner == repo.owner.login) { ref } val label = if (baseOwner == repo.owner.login) { ref } else { s"${repo.owner.login}:${ref}" }
else { s"${repo.owner.login}:${ref}" }
val user = repo.owner val user = repo.owner
} }

View File

@@ -40,12 +40,12 @@ object ApiRef {
ApiRef( ApiRef(
ref = s"refs/tags/${tagInfo.name}", ref = s"refs/tags/${tagInfo.name}",
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/refs/tags/${tagInfo.name}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/refs/tags/${tagInfo.name}"),
// the GH api distinguishes between "releases" and plain git tags //the GH api distinguishes between "releases" and plain git tags
// for "releases", the api returns a reference to the release object (with type `tag`) //for "releases", the api returns a reference to the release object (with type `tag`)
// this would be something like s"/api/v3/repos/${repositoryName.fullName}/git/tags/<hash-of-tag>" //this would be something like s"/api/v3/repos/${repositoryName.fullName}/git/tags/<hash-of-tag>"
// with a hash for the tag, which I do not fully understand //with a hash for the tag, which I do not fully understand
// since this is not yet implemented in GB, we always return a link to the plain `commit` object, //since this is not yet implemented in GB, we always return a link to the plain `commit` object,
// which GH does for tags that are not annotated //which GH does for tags that are not annotated
`object` = ApiRefCommit( `object` = ApiRefCommit(
sha = tagInfo.objectId, sha = tagInfo.objectId,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${tagInfo.objectId}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${tagInfo.objectId}"),

View File

@@ -21,7 +21,7 @@ case class ApiRepository(
val url = ApiPath(s"/api/v3/repos/${full_name}") val url = ApiPath(s"/api/v3/repos/${full_name}")
val clone_url = ApiPath(s"/git/${full_name}.git") val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}") val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s"/${full_name}.git")) val ssh_url = Some(SshPath(""))
} }
object ApiRepository { object ApiRepository {
@@ -61,7 +61,7 @@ object ApiRepository {
watchers = 0, watchers = 0,
forks = 0, forks = 0,
`private` = false, `private` = false,
default_branch = "main", default_branch = "master",
owner = owner, owner = owner,
has_issues = true has_issues = true
) )

View File

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

View File

@@ -24,8 +24,7 @@ object ApiUser {
def apply(user: Account): ApiUser = ApiUser( def apply(user: Account): ApiUser = ApiUser(
login = user.userName, login = user.userName,
email = user.mailAddress, email = user.mailAddress,
`type` = if (user.isGroupAccount) { "Organization" } `type` = if (user.isGroupAccount) { "Organization" } else { "User" },
else { "User" },
site_admin = user.isAdmin, site_admin = user.isAdmin,
created_at = user.registeredDate created_at = user.registeredDate
) )

View File

@@ -15,12 +15,13 @@ object JsonFormat {
val parserISO = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") val parserISO = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format => val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](
format =>
( (
{ case JString(s) => {
case JString(s) =>
Try(Date.from(Instant.parse(s))).getOrElse(throw new MappingException("Can't convert " + s + " to Date")) Try(Date.from(Instant.parse(s))).getOrElse(throw new MappingException("Can't convert " + s + " to Date"))
}, }, { case x: Date => JString(OffsetDateTime.ofInstant(x.toInstant, ZoneId.of("UTC")).format(parserISO)) }
{ case x: Date => JString(OffsetDateTime.ofInstant(x.toInstant, ZoneId.of("UTC")).format(parserISO)) }
) )
) + FieldSerializer[ApiUser]() + ) + FieldSerializer[ApiUser]() +
FieldSerializer[ApiGroup]() + FieldSerializer[ApiGroup]() +
@@ -44,35 +45,32 @@ object JsonFormat {
FieldSerializer[ApiCommits.File]() + FieldSerializer[ApiCommits.File]() +
FieldSerializer[ApiRelease]() + FieldSerializer[ApiRelease]() +
FieldSerializer[ApiReleaseAsset]() + FieldSerializer[ApiReleaseAsset]() +
ApiBranchProtectionResponse.enforcementLevelSerializer ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = def apiPathSerializer(c: Context) =
new CustomSerializer[ApiPath](_ => new CustomSerializer[ApiPath](
( _ =>
{ ({
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length)) case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath") case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, }, {
{ case ApiPath(path) => case ApiPath(path) => JString(c.baseUrl + path)
JString(c.baseUrl + path) })
}
)
) )
def sshPathSerializer(c: Context) = def sshPathSerializer(c: Context) =
new CustomSerializer[SshPath](_ => new CustomSerializer[SshPath](
( _ =>
{ ({
case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) => case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) =>
SshPath(s.substring(c.sshUrl.get.length)) SshPath(s.substring(c.sshUrl.get.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath") case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, }, {
{ case SshPath(path) => case SshPath(path) =>
c.sshUrl.map { sshUrl => c.sshUrl.map { sshUrl =>
JString(sshUrl + path) JString(sshUrl + path)
} getOrElse JNothing } getOrElse JNothing
} })
)
) )
/** /**

View File

@@ -38,11 +38,23 @@ class AccountController
with RequestCache with RequestCache
trait AccountControllerBase extends AccountManagementControllerBase { trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService & RepositoryService & ActivityService & WikiService & LabelsService & SshKeyService & self: AccountService
GpgKeyService & OneselfAuthenticator & UsersAuthenticator & GroupManagerAuthenticator & ReadableUsersAuthenticator & with RepositoryService
AccessTokenService & WebHookService & PrioritiesService & RepositoryCreationService => with ActivityService
with WikiService
with LabelsService
with SshKeyService
with GpgKeyService
with OneselfAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
with ReadableUsersAuthenticator
with AccessTokenService
with WebHookService
with PrioritiesService
with RepositoryCreationService =>
private case class AccountNewForm( case class AccountNewForm(
userName: String, userName: String,
password: String, password: String,
fullName: String, fullName: String,
@@ -53,7 +65,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
fileId: Option[String] fileId: Option[String]
) )
private case class AccountEditForm( case class AccountEditForm(
password: Option[String], password: Option[String],
fullName: String, fullName: String,
mailAddress: String, mailAddress: String,
@@ -64,15 +76,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
clearImage: Boolean clearImage: Boolean
) )
private case class SshKeyForm(title: String, publicKey: String) case class SshKeyForm(title: String, publicKey: String)
private case class GpgKeyForm(title: String, publicKey: String) case class GpgKeyForm(title: String, publicKey: String)
private case class PersonalTokenForm(note: String) case class PersonalTokenForm(note: String)
private case class SyntaxHighlighterThemeForm(theme: String) case class SyntaxHighlighterThemeForm(theme: String)
private val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password", text(required, maxlength(40)))), "password" -> trim(label("Password", text(required, maxlength(40)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))), "fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
@@ -85,7 +97,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"fileId" -> trim(label("File ID", optional(text()))) "fileId" -> trim(label("File ID", optional(text())))
)(AccountNewForm.apply) )(AccountNewForm.apply)
private val editForm = mapping( val editForm = mapping(
"password" -> trim(label("Password", optional(text(maxlength(40))))), "password" -> trim(label("Password", optional(text(maxlength(40))))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))), "fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))), "mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))),
@@ -98,41 +110,41 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"clearImage" -> trim(label("Clear image", boolean())) "clearImage" -> trim(label("Clear image", boolean()))
)(AccountEditForm.apply) )(AccountEditForm.apply)
private val sshKeyForm = mapping( val sshKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))), "title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim2(label("Key", text(required, validPublicKey))) "publicKey" -> trim2(label("Key", text(required, validPublicKey)))
)(SshKeyForm.apply) )(SshKeyForm.apply)
private val gpgKeyForm = mapping( val gpgKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))), "title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> label("Key", text(required, validGpgPublicKey)) "publicKey" -> label("Key", text(required, validGpgPublicKey))
)(GpgKeyForm.apply) )(GpgKeyForm.apply)
private val personalTokenForm = mapping( val personalTokenForm = mapping(
"note" -> trim(label("Token", text(required, maxlength(100)))) "note" -> trim(label("Token", text(required, maxlength(100))))
)(PersonalTokenForm.apply) )(PersonalTokenForm.apply)
private val syntaxHighlighterThemeForm = mapping( val syntaxHighlighterThemeForm = mapping(
"highlighterTheme" -> trim(label("Theme", text(required))) "highlighterTheme" -> trim(label("Theme", text(required)))
)(SyntaxHighlighterThemeForm.apply) )(SyntaxHighlighterThemeForm.apply)
private val resetPasswordEmailForm = mapping( val resetPasswordEmailForm = mapping(
"mailAddress" -> trim(label("Email", text(required))) "mailAddress" -> trim(label("Email", text(required)))
)(ResetPasswordEmailForm.apply) )(ResetPasswordEmailForm.apply)
private val resetPasswordForm = mapping( val resetPasswordForm = mapping(
"token" -> trim(label("Token", text(required))), "token" -> trim(label("Token", text(required))),
"password" -> trim(label("Password", text(required, maxlength(40)))) "password" -> trim(label("Password", text(required, maxlength(40))))
)(ResetPasswordForm.apply) )(ResetPasswordForm.apply)
private case class NewGroupForm( case class NewGroupForm(
groupName: String, groupName: String,
description: Option[String], description: Option[String],
url: Option[String], url: Option[String],
fileId: Option[String], fileId: Option[String],
members: String members: String
) )
private case class EditGroupForm( case class EditGroupForm(
groupName: String, groupName: String,
description: Option[String], description: Option[String],
url: Option[String], url: Option[String],
@@ -140,15 +152,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
members: String, members: String,
clearImage: Boolean clearImage: Boolean
) )
private case class ResetPasswordEmailForm( case class ResetPasswordEmailForm(
mailAddress: String mailAddress: String
) )
private case class ResetPasswordForm( case class ResetPasswordForm(
token: String, token: String,
password: String password: String
) )
private val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"description" -> trim(label("Group description", optional(text()))), "description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))), "url" -> trim(label("URL", optional(text(maxlength(200))))),
@@ -156,7 +168,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"members" -> trim(label("Members", text(required, members))) "members" -> trim(label("Members", text(required, members)))
)(NewGroupForm.apply) )(NewGroupForm.apply)
private val editGroupForm = mapping( val editGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier))),
"description" -> trim(label("Group description", optional(text()))), "description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))), "url" -> trim(label("URL", optional(text(maxlength(200))))),
@@ -165,7 +177,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"clearImage" -> trim(label("Clear image", boolean())) "clearImage" -> trim(label("Clear image", boolean()))
)(EditGroupForm.apply) )(EditGroupForm.apply)
private case class RepositoryCreationForm( case class RepositoryCreationForm(
owner: String, owner: String,
name: String, name: String,
description: Option[String], description: Option[String],
@@ -173,8 +185,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
initOption: String, initOption: String,
sourceUrl: Option[String] sourceUrl: Option[String]
) )
case class ForkRepositoryForm(owner: String, name: String)
private val newRepositoryForm = mapping( val newRepositoryForm = mapping(
"owner" -> trim(label("Owner", text(required, maxlength(100), identifier, existsAccount))), "owner" -> trim(label("Owner", text(required, maxlength(100), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))), "name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
"description" -> trim(label("Description", optional(text()))), "description" -> trim(label("Description", optional(text()))),
@@ -183,27 +196,34 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"sourceUrl" -> trim(label("Source URL", optionalRequired(_.value("initOption") == "COPY", text()))) "sourceUrl" -> trim(label("Source URL", optionalRequired(_.value("initOption") == "COPY", text())))
)(RepositoryCreationForm.apply) )(RepositoryCreationForm.apply)
private case class AccountForm(accountName: String) val forkRepositoryForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
private val accountForm = mapping( case class AccountForm(accountName: String)
val accountForm = mapping(
"account" -> trim(label("Group/User name", text(required, validAccountName))) "account" -> trim(label("Group/User name", text(required, validAccountName)))
)(AccountForm.apply) )(AccountForm.apply)
// for account web hook url addition. // for account web hook url addition.
private case class AccountWebHookForm( case class AccountWebHookForm(
url: String, url: String,
events: Set[WebHook.Event], events: Set[WebHook.Event],
ctype: WebHookContentType, ctype: WebHookContentType,
token: Option[String] token: Option[String]
) )
private def accountWebHookForm(update: Boolean) = def accountWebHookForm(update: Boolean) =
mapping( mapping(
"url" -> trim(label("url", text(required, accountWebHook(update)))), "url" -> trim(label("url", text(required, accountWebHook(update)))),
"events" -> accountWebhookEvents, "events" -> accountWebhookEvents,
"ctype" -> label("ctype", text()), "ctype" -> label("ctype", text()),
"token" -> optional(trim(label("token", text(maxlength(100))))) "token" -> optional(trim(label("token", text(maxlength(100)))))
)((url, events, ctype, token) => AccountWebHookForm(url, events, WebHookContentType.valueOf(ctype), token)) )(
(url, events, ctype, token) => AccountWebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
)
/** /**
* Provides duplication check for web hook url. duplicated from RepositorySettingsController.scala * Provides duplication check for web hook url. duplicated from RepositorySettingsController.scala
@@ -248,12 +268,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
gitbucket.core.account.html.activity( gitbucket.core.account.html.activity(
account, account,
if (account.isGroupAccount) Nil else getGroupsByUserName(userName), if (account.isGroupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, publicOnly = true), getActivitiesByUser(userName, true),
extraMailAddresses extraMailAddresses
) )
// Members // Members
case "members" if account.isGroupAccount => case "members" if (account.isGroupAccount) => {
val members = getGroupMembers(account.userName) val members = getGroupMembers(account.userName)
gitbucket.core.account.html.members( gitbucket.core.account.html.members(
account, account,
@@ -261,9 +281,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
extraMailAddresses, extraMailAddresses,
isGroupManager(context.loginAccount, members) isGroupManager(context.loginAccount, members)
) )
}
// Repositories // Repositories
case _ => case _ => {
val members = getGroupMembers(account.userName) val members = getGroupMembers(account.userName)
gitbucket.core.account.html.repositories( gitbucket.core.account.html.repositories(
account, account,
@@ -273,13 +294,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
isGroupManager(context.loginAccount, members) isGroupManager(context.loginAccount, members)
) )
} }
}
} getOrElse NotFound() } getOrElse NotFound()
} }
get("/:userName.atom") { get("/:userName.atom") {
val userName = params("userName") val userName = params("userName")
contentType = "application/atom+xml; type=feed" contentType = "application/atom+xml; type=feed"
helper.xml.feed(getActivitiesByUser(userName, publicOnly = true)) helper.xml.feed(getActivitiesByUser(userName, true))
} }
get("/:userName.keys") { get("/:userName.keys") {
@@ -324,7 +346,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:userName/_edit", editForm)(oneselfOnly { form => post("/:userName/_edit", editForm)(oneselfOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map {
account =>
updateAccount( updateAccount(
account.copy( account.copy(
password = form.password.map(pbkdf2_sha256).getOrElse(account.password), password = form.password.map(pbkdf2_sha256).getOrElse(account.password),
@@ -338,7 +361,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
updateImage(userName, form.fileId, form.clearImage) updateImage(userName, form.fileId, form.clearImage)
updateAccountExtraMailAddresses(userName, form.extraMailAddresses.filter(_ != "")) updateAccountExtraMailAddresses(userName, form.extraMailAddresses.filter(_ != ""))
flash.update("info", "Account information has been updated.") flash.update("info", "Account information has been updated.")
redirect(s"/$userName/_edit") redirect(s"/${userName}/_edit")
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -346,10 +369,11 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_delete")(oneselfOnly { get("/:userName/_delete")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName, includeRemoved = true).map { account => getAccountByUserName(userName, true).map {
account =>
if (isLastAdministrator(account)) { if (isLastAdministrator(account)) {
flash.update("error", "Account can't be removed because this is last one administrator.") flash.update("error", "Account can't be removed because this is last one administrator.")
redirect(s"/$userName/_edit") redirect(s"/${userName}/_edit")
} else { } else {
// // Remove repositories // // Remove repositories
// getRepositoryNamesOfUser(userName).foreach { repositoryName => // getRepositoryNamesOfUser(userName).foreach { repositoryName =>
@@ -359,7 +383,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) // FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// } // }
suspendAccount(account) suspendAccount(account)
session.invalidate() session.invalidate
redirect("/") redirect("/")
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -375,20 +399,20 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form => post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
val userName = params("userName") val userName = params("userName")
addPublicKey(userName, form.title, form.publicKey) addPublicKey(userName, form.title, form.publicKey)
redirect(s"/$userName/_ssh") redirect(s"/${userName}/_ssh")
}) })
get("/:userName/_ssh/delete/:id")(oneselfOnly { get("/:userName/_ssh/delete/:id")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
val sshKeyId = params("id").toInt val sshKeyId = params("id").toInt
deletePublicKey(userName, sshKeyId) deletePublicKey(userName, sshKeyId)
redirect(s"/$userName/_ssh") redirect(s"/${userName}/_ssh")
}) })
get("/:userName/_gpg")(oneselfOnly { get("/:userName/_gpg")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { x =>
// html.ssh(x, getPublicKeys(x.userName)) //html.ssh(x, getPublicKeys(x.userName))
html.gpg(x, getGpgPublicKeys(x.userName)) html.gpg(x, getGpgPublicKeys(x.userName))
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -396,14 +420,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:userName/_gpg", gpgKeyForm)(oneselfOnly { form => post("/:userName/_gpg", gpgKeyForm)(oneselfOnly { form =>
val userName = params("userName") val userName = params("userName")
addGpgPublicKey(userName, form.title, form.publicKey) addGpgPublicKey(userName, form.title, form.publicKey)
redirect(s"/$userName/_gpg") redirect(s"/${userName}/_gpg")
}) })
get("/:userName/_gpg/delete/:id")(oneselfOnly { get("/:userName/_gpg/delete/:id")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
val keyId = params("id").toInt val keyId = params("id").toInt
deleteGpgPublicKey(userName, keyId) deleteGpgPublicKey(userName, keyId)
redirect(s"/$userName/_gpg") redirect(s"/${userName}/_gpg")
}) })
get("/:userName/_application")(oneselfOnly { get("/:userName/_application")(oneselfOnly {
@@ -411,12 +435,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { x =>
var tokens = getAccessTokens(x.userName) var tokens = getAccessTokens(x.userName)
val generatedToken = flash.get("generatedToken") match { val generatedToken = flash.get("generatedToken") match {
case Some((tokenId: Int, token: String)) => case Some((tokenId: Int, token: String)) => {
val gt = tokens.find(_.accessTokenId == tokenId) val gt = tokens.find(_.accessTokenId == tokenId)
gt.map { t => gt.map { t =>
tokens = tokens.filterNot(_ == t) tokens = tokens.filterNot(_ == t)
(t, token) (t, token)
} }
}
case _ => None case _ => None
} }
html.application(x, tokens, generatedToken) html.application(x, tokens, generatedToken)
@@ -425,18 +450,18 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form => post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).foreach { _ => getAccountByUserName(userName).foreach { x =>
val (tokenId, token) = generateAccessToken(userName, form.note) val (tokenId, token) = generateAccessToken(userName, form.note)
flash.update("generatedToken", (tokenId, token)) flash.update("generatedToken", (tokenId, token))
} }
redirect(s"/$userName/_application") redirect(s"/${userName}/_application")
}) })
get("/:userName/_personalToken/delete/:id")(oneselfOnly { get("/:userName/_personalToken/delete/:id")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
val tokenId = params("id").toInt val tokenId = params("id").toInt
deleteAccessToken(userName, tokenId) deleteAccessToken(userName, tokenId)
redirect(s"/$userName/_application") redirect(s"/${userName}/_application")
}) })
/** /**
@@ -459,7 +484,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:userName/_preferences/highlighter", syntaxHighlighterThemeForm)(oneselfOnly { form => post("/:userName/_preferences/highlighter", syntaxHighlighterThemeForm)(oneselfOnly { form =>
val userName = params("userName") val userName = params("userName")
addOrUpdateAccountPreference(userName, form.theme) addOrUpdateAccountPreference(userName, form.theme)
redirect(s"/$userName/_preferences") redirect(s"/${userName}/_preferences")
}) })
get("/:userName/_hooks")(managersOnly { get("/:userName/_hooks")(managersOnly {
@@ -476,7 +501,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
val webhook = AccountWebHook(userName, "", WebHookContentType.FORM, None) val webhook = AccountWebHook(userName, "", WebHookContentType.FORM, None)
html.edithook(webhook, Set(WebHook.Push), account, create = true) html.edithook(webhook, Set(WebHook.Push), account, true)
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -487,7 +512,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName") val userName = params("userName")
addAccountWebHook(userName, form.url, form.events, form.ctype, form.token) addAccountWebHook(userName, form.url, form.events, form.ctype, form.token)
flash.update("info", s"Webhook ${form.url} created") flash.update("info", s"Webhook ${form.url} created")
redirect(s"/$userName/_hooks") redirect(s"/${userName}/_hooks")
}) })
/** /**
@@ -497,7 +522,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName") val userName = params("userName")
deleteAccountWebHook(userName, params("url")) deleteAccountWebHook(userName, params("url"))
flash.update("info", s"Webhook ${params("url")} deleted") flash.update("info", s"Webhook ${params("url")} deleted")
redirect(s"/$userName/_hooks") redirect(s"/${userName}/_hooks")
}) })
/** /**
@@ -506,8 +531,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_hooks/edit")(managersOnly { get("/:userName/_hooks/edit")(managersOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).flatMap { account => getAccountByUserName(userName).flatMap { account =>
getAccountWebHook(userName, params("url")).map { case (webhook, events) => getAccountWebHook(userName, params("url")).map {
html.edithook(webhook, events, account, create = false) case (webhook, events) =>
html.edithook(webhook, events, account, false)
} }
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -519,7 +545,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName") val userName = params("userName")
updateAccountWebHook(userName, form.url, form.events, form.ctype, form.token) updateAccountWebHook(userName, form.url, form.events, form.ctype, form.token)
flash.update("info", s"webhook ${form.url} updated") flash.update("info", s"webhook ${form.url} updated")
redirect(s"/$userName/_hooks") redirect(s"/${userName}/_hooks")
}) })
/** /**
@@ -527,8 +553,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
*/ */
ajaxPost("/:userName/_hooks/test")(managersOnly { ajaxPost("/:userName/_hooks/test")(managersOnly {
// TODO Is it possible to merge with [[RepositorySettingsController.ajaxPost]]? // TODO Is it possible to merge with [[RepositorySettingsController.ajaxPost]]?
import scala.concurrent.duration.* import scala.concurrent.duration._
import scala.concurrent.* import scala.concurrent._
import scala.util.control.NonFatal import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@@ -552,10 +578,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head
val toErrorMap: PartialFunction[Throwable, Map[String, String]] = { val toErrorMap: PartialFunction[Throwable, Map[String, String]] = {
case e: java.net.UnknownHostException => Map("error" -> s"Unknown host ${e.getMessage}") case e: java.net.UnknownHostException => Map("error" -> ("Unknown host " + e.getMessage))
case _: java.lang.IllegalArgumentException => Map("error" -> "invalid url") case e: java.lang.IllegalArgumentException => Map("error" -> ("invalid url"))
case _: org.apache.http.client.ClientProtocolException => Map("error" -> "invalid url") case e: org.apache.http.client.ClientProtocolException => Map("error" -> ("invalid url"))
case NonFatal(e) => Map("error" -> s"${e.getClass} ${e.getMessage}") case NonFatal(e) => Map("error" -> (s"${e.getClass} ${e.getMessage}"))
} }
contentType = formats("json") contentType = formats("json")
@@ -564,7 +590,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"url" -> url, "url" -> url,
"request" -> Await.result( "request" -> Await.result(
reqFuture reqFuture
.map(req => .map(
req =>
Map( Map(
"headers" -> _headers(req.getAllHeaders), "headers" -> _headers(req.getAllHeaders),
"payload" -> json "payload" -> json
@@ -575,11 +602,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
), ),
"response" -> Await.result( "response" -> Await.result(
resFuture resFuture
.map(res => .map(
res =>
Map( Map(
"status" -> res.getStatusLine, "status" -> res.getStatusLine(),
"body" -> EntityUtils.toString(res.getEntity), "body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders) "headers" -> _headers(res.getAllHeaders())
) )
) )
.recover(toErrorMap), .recover(toErrorMap),
@@ -606,11 +634,11 @@ trait AccountControllerBase extends AccountManagementControllerBase {
pbkdf2_sha256(form.password), pbkdf2_sha256(form.password),
form.fullName, form.fullName,
form.mailAddress, form.mailAddress,
isAdmin = false, false,
form.description, form.description,
form.url form.url
) )
updateImage(form.userName, form.fileId, clearImage = false) updateImage(form.userName, form.fileId, false)
updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != "")) updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != ""))
redirect("/signin") redirect("/signin")
} else NotFound() } else NotFound()
@@ -635,7 +663,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|You requested to reset the password for your GitBucket account. |You requested to reset the password for your GitBucket account.
|If you are not sure about the request, you can ignore this email. |If you are not sure about the request, you can ignore this email.
|Otherwise, click the following link to set the new password: |Otherwise, click the following link to set the new password:
|${context.baseUrl}/reset/form/$token |${context.baseUrl}/reset/form/${token}
|""".stripMargin |""".stripMargin
) )
} }
@@ -675,7 +703,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/groups/new")(usersOnly { get("/groups/new")(usersOnly {
context.withLoginAccount { loginAccount => context.withLoginAccount { loginAccount =>
html.creategroup(List(GroupMember("", loginAccount.userName, isManager = true))) html.creategroup(List(GroupMember("", loginAccount.userName, true)))
} }
}) })
@@ -692,13 +720,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
.toList .toList
) )
updateImage(form.groupName, form.fileId, clearImage = false) updateImage(form.groupName, form.fileId, false)
redirect(s"/${form.groupName}") redirect(s"/${form.groupName}")
}) })
get("/:groupName/_editgroup")(managersOnly { get("/:groupName/_editgroup")(managersOnly {
val groupName = params("groupName") val groupName = params("groupName")
getAccountByUserName(groupName, includeRemoved = true).map { account => getAccountByUserName(groupName, true).map { account =>
html.editgroup(account, getGroupMembers(groupName), flash.get("info")) html.editgroup(account, getGroupMembers(groupName), flash.get("info"))
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -708,8 +736,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Remove from GROUP_MEMBER // Remove from GROUP_MEMBER
updateGroupMembers(groupName, Nil) updateGroupMembers(groupName, Nil)
// Disable group // Disable group
getAccountByUserName(groupName, includeRemoved = false).foreach { account => getAccountByUserName(groupName, false).foreach { account =>
updateGroup(groupName, account.description, account.url, removed = true) updateGroup(groupName, account.description, account.url, true)
} }
// // Remove repositories // // Remove repositories
// getRepositoryNamesOfUser(groupName).foreach { repositoryName => // getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
@@ -732,8 +760,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
.toList .toList
getAccountByUserName(groupName, includeRemoved = true).map { _ => getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.description, form.url, removed = false) updateGroup(groupName, form.description, form.url, false)
// Update GROUP_MEMBER // Update GROUP_MEMBER
updateGroupMembers(form.groupName, members) updateGroupMembers(form.groupName, members)
@@ -748,7 +776,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
updateImage(form.groupName, form.fileId, form.clearImage) updateImage(form.groupName, form.fileId, form.clearImage)
flash.update("info", "Account information has been updated.") flash.update("info", "Account information has been updated.")
redirect(s"/$groupName/_editgroup") redirect(s"/${groupName}/_editgroup")
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -766,7 +794,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.create || loginAccount.isAdmin) { if (context.settings.basicBehavior.repositoryOperation.create || loginAccount.isAdmin) {
LockUtil.lock(s"${form.owner}/${form.name}") { LockUtil.lock(s"${form.owner}/${form.name}") {
if (getRepository(form.owner, form.name).isDefined) { if (getRepository(form.owner, form.name).isDefined) {
@@ -784,8 +813,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
form.description, form.description,
form.isPrivate, form.isPrivate,
form.initOption, form.initOption,
form.sourceUrl, form.sourceUrl
context.settings.defaultBranch
) )
// redirect to the repository // redirect to the repository
redirect(s"/${form.owner}/${form.name}") redirect(s"/${form.owner}/${form.name}")
@@ -796,17 +824,17 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}) })
get("/:owner/:repository/fork")(readableUsersOnly { repository => get("/:owner/:repository/fork")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
if ( loginAccount =>
repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin) if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
) {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName) val groups = getGroupsByUserName(loginUserName)
groups match { groups match {
case _: List[String] => case _: List[String] =>
val managerPermissions = groups.map { group => val managerPermissions = groups.map { group =>
val members = getGroupMembers(group) val members = getGroupMembers(group)
context.loginAccount.exists(x => context.loginAccount.exists(
x =>
members.exists { member => members.exists { member =>
member.userName == x.userName && member.isManager member.userName == x.userName && member.isManager
} }
@@ -816,23 +844,22 @@ trait AccountControllerBase extends AccountManagementControllerBase {
repository, repository,
(groups zip managerPermissions).sortBy(_._1) (groups zip managerPermissions).sortBy(_._1)
) )
case _ => redirect(s"/$loginUserName") case _ => redirect(s"/${loginUserName}")
} }
} else BadRequest() } else BadRequest()
} }
}) })
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
if ( loginAccount =>
repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin) if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
) {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val accountName = form.accountName val accountName = form.accountName
if (getRepository(accountName, repository.name).isDefined) { if (getRepository(accountName, repository.name).isDefined) {
// redirect to the repository if repository already exists // redirect to the repository if repository already exists
redirect(s"/$accountName/${repository.name}") redirect(s"/${accountName}/${repository.name}")
} else if (!canCreateRepository(accountName, loginAccount)) { } else if (!canCreateRepository(accountName, loginAccount)) {
// Permission error // Permission error
Forbidden() Forbidden()
@@ -840,7 +867,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// fork repository asynchronously // fork repository asynchronously
forkRepository(accountName, repository, loginUserName) forkRepository(accountName, repository, loginUserName)
// redirect to the repository // redirect to the repository
redirect(s"/$accountName/${repository.name}") redirect(s"/${accountName}/${repository.name}")
} }
} else Forbidden() } else Forbidden()
} }
@@ -869,11 +896,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
private def members: Constraint = new Constraint() { private def members: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
if ( if (value.split(",").exists {
value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean } _.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
} }) None
) None
else Some("Must select one manager at least.") else Some("Must select one manager at least.")
} }
} }

View File

@@ -1,10 +1,10 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api.* import gitbucket.core.api._
import gitbucket.core.controller.api.* import gitbucket.core.controller.api._
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
class ApiController class ApiController
@@ -100,4 +100,16 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/gitbucket/plugins") { get("/api/v3/gitbucket/plugins") {
PluginRegistry().getPlugins().map { ApiPlugin(_) } PluginRegistry().getPlugins().map { ApiPlugin(_) }
} }
/**
* https://docs.github.com/en/enterprise-server@2.21/rest/reference/meta#get-github-enterprise-server-meta-information
*/
get("/api/v3/meta") {
JsonFormat(
Map(
"https://api.github.com/meta" -> context.loginAccount.isDefined,
"installed_version" -> "2.21.0"
)
)
}
} }

View File

@@ -1,21 +1,22 @@
package gitbucket.core.controller package gitbucket.core.controller
import java.io.{File, FileInputStream, FileOutputStream} import java.io.{File, FileInputStream}
import gitbucket.core.api.{ApiError, JsonFormat} import gitbucket.core.api.{ApiError, JsonFormat}
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService} import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
import gitbucket.core.util.SyntaxSugars.* import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.* import gitbucket.core.util._
import org.json4s.* import org.json4s._
import org.scalatra.{MultiParams, *} import org.scalatra._
import org.scalatra.i18n.* import org.scalatra.i18n._
import org.scalatra.json.* import org.scalatra.json._
import org.scalatra.forms.* import org.scalatra.forms._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import javax.servlet.{FilterChain, ServletRequest, ServletResponse} import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
import is.tagomor.woothee.Classifier import is.tagomor.woothee.Classifier
import scala.util.Try import scala.util.Try
@@ -24,13 +25,10 @@ import net.coobird.thumbnailator.Thumbnails
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk.* import org.eclipse.jgit.treewalk._
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.json4s.Formats import org.json4s.Formats
import org.json4s.jackson.Serialization
import java.nio.charset.StandardCharsets
/** /**
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
@@ -48,21 +46,11 @@ abstract class ControllerBase
implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats
private case class HttpException(status: Int) extends RuntimeException
before("/api/v3/*") { before("/api/v3/*") {
contentType = formats("json") contentType = formats("json")
request.setAttribute(Keys.Request.APIv3, true) request.setAttribute(Keys.Request.APIv3, true)
} }
override def multiParams(implicit request: HttpServletRequest): MultiParams = {
try {
super.multiParams
} catch {
case _: Exception => throw HttpException(400)
}
}
override def requestPath(uri: String, idx: Int): String = { override def requestPath(uri: String, idx: Int): String = {
val path = super.requestPath(uri, idx) val path = super.requestPath(uri, idx)
if (path != "/" && path.endsWith("/")) { if (path != "/" && path.endsWith("/")) {
@@ -96,24 +84,17 @@ abstract class ControllerBase
*/ */
implicit def context: Context = { implicit def context: Context = {
contextCache.get match { contextCache.get match {
case null => case null => {
val context = Context(loadSystemSettings(), LoginAccount, request) val context = Context(loadSystemSettings(), LoginAccount, request)
contextCache.set(context) contextCache.set(context)
context context
}
case context => context case context => context
} }
} }
private def LoginAccount: Option[Account] = { private def LoginAccount: Option[Account] =
request request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
.getAs[Account](Keys.Session.LoginAccount)
.orElse(session.getAs[Account](Keys.Session.LoginAccount))
.orElse {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
getLoginAccountFromLocalFile()
} else None
}
}
def ajaxGet(path: String)(action: => Any): Route = def ajaxGet(path: String)(action: => Any): Route =
super.get(path) { super.get(path) {
@@ -139,7 +120,7 @@ abstract class ControllerBase
action(form) action(form)
} }
protected def NotFound(): ActionResult = protected def NotFound() =
if (request.hasAttribute(Keys.Request.Ajax)) { if (request.hasAttribute(Keys.Request.Ajax)) {
org.scalatra.NotFound() org.scalatra.NotFound()
} else if (request.hasAttribute(Keys.Request.APIv3)) { } else if (request.hasAttribute(Keys.Request.APIv3)) {
@@ -159,7 +140,7 @@ abstract class ControllerBase
} }
} }
protected def Unauthorized()(implicit context: Context): ActionResult = protected def Unauthorized()(implicit context: Context) =
if (request.hasAttribute(Keys.Request.Ajax)) { if (request.hasAttribute(Keys.Request.Ajax)) {
org.scalatra.Unauthorized() org.scalatra.Unauthorized()
} else if (request.hasAttribute(Keys.Request.APIv3)) { } else if (request.hasAttribute(Keys.Request.APIv3)) {
@@ -187,9 +168,7 @@ abstract class ControllerBase
} }
error { error {
case e: HttpException => case e => {
ActionResult(e.status, (), Map.empty)
case e =>
logger.error(s"Catch unhandled error in request: ${request}", e) logger.error(s"Catch unhandled error in request: ${request}", e)
if (request.hasAttribute(Keys.Request.Ajax)) { if (request.hasAttribute(Keys.Request.Ajax)) {
org.scalatra.InternalServerError() org.scalatra.InternalServerError()
@@ -200,6 +179,7 @@ abstract class ControllerBase
org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e))) org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e)))
} }
} }
}
override def url( override def url(
path: String, path: String,
@@ -210,7 +190,7 @@ abstract class ControllerBase
withSessionId: Boolean = true withSessionId: Boolean = true
)(implicit request: HttpServletRequest, response: HttpServletResponse): String = )(implicit request: HttpServletRequest, response: HttpServletResponse): String =
if (path.startsWith("http")) path if (path.startsWith("http")) path
else baseUrl + super.url(path, params, includeContextPath = false, includeServletPath = false, absolutize = false) else baseUrl + super.url(path, params, false, false, false)
/** /**
* Extends scalatra-form's trim rule to eliminate CR and LF. * Extends scalatra-form's trim rule to eliminate CR and LF.
@@ -254,7 +234,7 @@ abstract class ControllerBase
protected def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = { protected def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@scala.annotation.tailrec @scala.annotation.tailrec
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if walk.getPathString == path => Some(walk.getObjectId(0)) case true if (walk.getPathString == path) => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk) case true => _getPathObjectId(path, walk)
case false => None case false => None
} }
@@ -297,47 +277,6 @@ abstract class ControllerBase
} }
} }
} }
protected object DevFeatures {
val KeepSession = "keep-session"
}
private val loginAccountFile = new File(".tmp/login_account.json")
protected def isDevFeatureEnabled(feature: String): Boolean = {
Option(System.getProperty("dev-features")).getOrElse("").split(",").map(_.trim).contains(feature)
}
protected def getLoginAccountFromLocalFile(): Option[Account] = {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
if (loginAccountFile.exists()) {
Using.resource(new FileInputStream(loginAccountFile)) { in =>
val json = IOUtils.toString(in, StandardCharsets.UTF_8)
val account = parse(json).extract[Account]
session.setAttribute(Keys.Session.LoginAccount, account)
Some(parse(json).extract[Account])
}
} else None
} else None
}
protected def saveLoginAccountToLocalFile(account: Account): Unit = {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
if (!loginAccountFile.getParentFile.exists()) {
loginAccountFile.getParentFile.mkdirs()
}
Using.resource(new FileOutputStream(loginAccountFile)) { in =>
in.write(Serialization.write(account).getBytes(StandardCharsets.UTF_8))
}
}
}
protected def deleteLoginAccountFromLocalFile(): Unit = {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
loginAccountFile.delete()
}
}
} }
/** /**
@@ -348,18 +287,18 @@ case class Context(
loginAccount: Option[Account], loginAccount: Option[Account],
request: HttpServletRequest request: HttpServletRequest
) { ) {
val path: String = settings.baseUrl.getOrElse(request.getContextPath) val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath: String = request.getRequestURI.substring(request.getContextPath.length) val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl: String = settings.baseUrl(request) val baseUrl = settings.baseUrl(request)
val host: String = new java.net.URL(baseUrl).getHost val host = new java.net.URL(baseUrl).getHost
val platform: String = request.getHeader("User-Agent") match { val platform = 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"
case agent if agent.contains("Linux") => "linux" case agent if agent.contains("Linux") => "linux"
case agent if agent.contains("Win") => "windows" case agent if agent.contains("Win") => "windows"
case _ => null case _ => null
} }
val sidebarCollapse: Boolean = request.getSession.getAttribute("sidebar-collapse") != null val sidebarCollapse = request.getSession.getAttribute("sidebar-collapse") != null
def withLoginAccount(f: Account => Any): Any = { def withLoginAccount(f: Account => Any): Any = {
loginAccount match { loginAccount match {
@@ -433,16 +372,15 @@ trait AccountManagementControllerBase extends ControllerBase {
messages: Messages messages: Messages
): Option[String] = { ): Option[String] = {
val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses")) val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses"))
if ( if (extraMailAddresses.exists {
extraMailAddresses.exists { case (k, v) => case (k, v) =>
v.contains(value) v.contains(value)
} }) {
) {
Some("These mail addresses are duplicated.") Some("These mail addresses are duplicated.")
} else { } else {
getAccountByMailAddress(value, includeRemoved = true) getAccountByMailAddress(value, true)
.collect { .collect {
case x if paramName.isEmpty || !params.optionValue(paramName).contains(x.userName) => case x if paramName.isEmpty || Some(x.userName) != params.optionValue(paramName) =>
"Mail address is already registered." "Mail address is already registered."
} }
} }
@@ -457,23 +395,22 @@ trait AccountManagementControllerBase extends ControllerBase {
messages: Messages messages: Messages
): Option[String] = { ): Option[String] = {
val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses")) val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses"))
if ( if (Some(value) == params.optionValue("mailAddress") || extraMailAddresses.count {
params.optionValue("mailAddress").contains(value) || extraMailAddresses.count { case (k, v) => case (k, v) =>
v.contains(value) v.contains(value)
} > 1 } > 1) {
) {
Some("These mail addresses are duplicated.") Some("These mail addresses are duplicated.")
} else { } else {
getAccountByMailAddress(value, includeRemoved = true) getAccountByMailAddress(value, true)
.collect { .collect {
case x if paramName.isEmpty || !params.optionValue(paramName).contains(x.userName) => case x if paramName.isEmpty || Some(x.userName) != params.optionValue(paramName) =>
"Mail address is already registered." "Mail address is already registered."
} }
} }
} }
} }
private val allReservedNames = Set( val allReservedNames = Set(
"git", "git",
"admin", "admin",
"upload", "upload",
@@ -492,9 +429,10 @@ trait AccountManagementControllerBase extends ControllerBase {
protected def reservedNames: Constraint = new Constraint() { protected def reservedNames: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (allReservedNames.contains(value.toLowerCase)) { if (allReservedNames.contains(value.toLowerCase)) {
Some(s"$value is reserved") Some(s"${value} is reserved")
} else { } else {
None None
} }
} }
} }

View File

@@ -2,11 +2,10 @@ package gitbucket.core.controller
import gitbucket.core.dashboard.html import gitbucket.core.dashboard.html
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.util.UsersAuthenticator import gitbucket.core.util.{Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.service.IssuesService.* import gitbucket.core.service.IssuesService._
import gitbucket.core.service.ActivityService.*
class DashboardController class DashboardController
extends DashboardControllerBase extends DashboardControllerBase
@@ -28,8 +27,12 @@ class DashboardController
with RequestCache with RequestCache
trait DashboardControllerBase extends ControllerBase { trait DashboardControllerBase extends ControllerBase {
self: IssuesService & PullRequestService & RepositoryService & AccountService & CommitStatusService & self: IssuesService
UsersAuthenticator => with PullRequestService
with RepositoryService
with AccountService
with CommitStatusService
with UsersAuthenticator =>
get("/dashboard/repos")(usersOnly { get("/dashboard/repos")(usersOnly {
context.withLoginAccount { loginAccount => context.withLoginAccount { loginAccount =>
@@ -39,7 +42,7 @@ trait DashboardControllerBase extends ControllerBase {
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
) )
html.repos(getGroupNames(loginAccount.userName), repos, repos, isNewsFeedEnabled) html.repos(getGroupNames(loginAccount.userName), repos, repos)
} }
}) })
@@ -91,7 +94,7 @@ trait DashboardControllerBase extends ControllerBase {
} }
}) })
private def getOrCreateCondition(filter: String, userName: String) = { private def getOrCreateCondition(key: String, filter: String, userName: String) = {
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
filter match { filter match {
@@ -102,19 +105,19 @@ trait DashboardControllerBase extends ControllerBase {
} }
private def searchIssues(loginAccount: Account, filter: String) = { private def searchIssues(loginAccount: Account, filter: String) = {
import IssuesService.* import IssuesService._
val userName = loginAccount.userName val userName = loginAccount.userName
val condition = getOrCreateCondition(filter, userName) val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, withoutPhysicalInfo = true).map(repo => repo.owner -> repo.name) val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val issues = searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, userRepos*) val issues = searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, userRepos: _*)
html.issues( html.issues(
issues.map(issue => (issue, None)), issues.map(issue => (issue, None)),
page, page,
countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, userRepos*), countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, userRepos: _*),
countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, userRepos*), countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, userRepos: _*),
filter match { filter match {
case "assigned" => condition.copy(assigned = Some(Some(userName))) case "assigned" => condition.copy(assigned = Some(Some(userName)))
case "mentioned" => condition.copy(mentioned = Some(userName)) case "mentioned" => condition.copy(mentioned = Some(userName))
@@ -127,17 +130,16 @@ trait DashboardControllerBase extends ControllerBase {
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
), )
isNewsFeedEnabled
) )
} }
private def searchPullRequests(loginAccount: Account, filter: String) = { private def searchPullRequests(loginAccount: Account, filter: String) = {
import IssuesService.* import IssuesService._
import PullRequestService.* import PullRequestService._
val userName = loginAccount.userName val userName = loginAccount.userName
val condition = getOrCreateCondition(filter, userName) val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
val allRepos = getAllRepositories(userName) val allRepos = getAllRepositories(userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val issues = searchIssue( val issues = searchIssue(
@@ -145,7 +147,7 @@ trait DashboardControllerBase extends ControllerBase {
IssueSearchOption.PullRequests, IssueSearchOption.PullRequests,
(page - 1) * PullRequestLimit, (page - 1) * PullRequestLimit,
PullRequestLimit, PullRequestLimit,
allRepos* allRepos: _*
) )
val status = issues.map { issue => val status = issues.map { issue =>
issue.commitId.flatMap { commitId => issue.commitId.flatMap { commitId =>
@@ -156,8 +158,8 @@ trait DashboardControllerBase extends ControllerBase {
html.pulls( html.pulls(
issues.zip(status), issues.zip(status),
page, page,
countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, allRepos*), countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, allRepos: _*),
countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, allRepos*), countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, allRepos: _*),
filter match { filter match {
case "assigned" => condition.copy(assigned = Some(Some(userName))) case "assigned" => condition.copy(assigned = Some(Some(userName)))
case "mentioned" => condition.copy(mentioned = Some(userName)) case "mentioned" => condition.copy(mentioned = Some(userName))
@@ -170,8 +172,7 @@ trait DashboardControllerBase extends ControllerBase {
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
), )
isNewsFeedEnabled
) )
} }

View File

@@ -75,7 +75,8 @@ class FileUploadController
post("/wiki/:owner/:repository") { post("/wiki/:owner/:repository") {
setMultipartConfig() setMultipartConfig()
// Don't accept not logged-in users // Don't accept not logged-in users
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account => session.get(Keys.Session.LoginAccount).collect {
case loginAccount: Account =>
val owner = params("owner") val owner = params("owner")
val repository = params("repository") val repository = params("repository")
@@ -84,8 +85,9 @@ class FileUploadController
execute( execute(
{ (file, fileId) => { (file, fileId) =>
val fileName = file.getName val fileName = file.getName
LockUtil.lock(s"$owner/$repository/wiki") { LockUtil.lock(s"${owner}/${repository}/wiki") {
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git => Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) {
git =>
val builder = DirCache.newInCore.builder() val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter() val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
@@ -108,7 +110,7 @@ class FileUploadController
) )
builder.finish() builder.finish()
JGitUtil.createNewCommit( val newHeadId = JGitUtil.createNewCommit(
git, git,
inserter, inserter,
headId, headId,
@@ -116,7 +118,7 @@ class FileUploadController
Constants.HEAD, Constants.HEAD,
loginAccount.fullName, loginAccount.fullName,
loginAccount.mailAddress, loginAccount.mailAddress,
s"Uploaded $fileName" s"Uploaded ${fileName}"
) )
fileName fileName
@@ -133,7 +135,8 @@ class FileUploadController
setMultipartConfigForLargeFile() setMultipartConfigForLargeFile()
session session
.get(Keys.Session.LoginAccount) .get(Keys.Session.LoginAccount)
.collect { case _: Account => .collect {
case _: Account =>
val owner = params("owner") val owner = params("owner")
val repository = params("repository") val repository = params("repository")
val tag = multiParams("splat").head val tag = multiParams("splat").head
@@ -151,16 +154,13 @@ class FileUploadController
} }
post("/import") { post("/import") {
import JDBCUtil.* import JDBCUtil._
setMultipartConfig() setMultipartConfig()
session.get(Keys.Session.LoginAccount).collect { session.get(Keys.Session.LoginAccount).collect {
case loginAccount: Account if loginAccount.isAdmin => case loginAccount: Account if loginAccount.isAdmin =>
execute( execute({ (file, fileId) =>
{ (file, fileId) =>
request2Session(request).conn.importAsSQL(file.getInputStream) request2Session(request).conn.importAsSQL(file.getInputStream)
}, }, _ => true)
_ => true
)
} }
redirect("/admin/data") redirect("/admin/data")
} }
@@ -168,13 +168,13 @@ class FileUploadController
private def setMultipartConfig(): Unit = { private def setMultipartConfig(): Unit = {
val settings = loadSystemSettings() val settings = loadSystemSettings()
val config = MultipartConfig(maxFileSize = Some(settings.upload.maxFileSize)) val config = MultipartConfig(maxFileSize = Some(settings.upload.maxFileSize))
config.apply(request.getServletContext) config.apply(request.getServletContext())
} }
private def setMultipartConfigForLargeFile(): Unit = { private def setMultipartConfigForLargeFile(): Unit = {
val settings = loadSystemSettings() val settings = loadSystemSettings()
val config = MultipartConfig(maxFileSize = Some(settings.upload.largeMaxFileSize)) val config = MultipartConfig(maxFileSize = Some(settings.upload.largeMaxFileSize))
config.apply(request.getServletContext) config.apply(request.getServletContext())
} }
private def onlyWikiEditable(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = { private def onlyWikiEditable(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = {
@@ -191,7 +191,7 @@ class FileUploadController
} }
} }
private def execute(f: (FileItem, String) => Unit, mimeTypeChecker: String => Boolean) = private def execute(f: (FileItem, String) => Unit, mimeTypeChecker: (String) => Boolean) =
fileParams.get("file") match { fileParams.get("file") match {
case Some(file) if mimeTypeChecker(file.name) => case Some(file) if mimeTypeChecker(file.name) =>
val fileId = FileUtil.generateFileId val fileId = FileUtil.generateFileId

View File

@@ -1,20 +1,17 @@
package gitbucket.core.controller package gitbucket.core.controller
import com.nimbusds.jwt.JWT
import java.net.URI import java.net.URI
import com.nimbusds.oauth2.sdk.id.State import com.nimbusds.oauth2.sdk.id.State
import com.nimbusds.openid.connect.sdk.Nonce import com.nimbusds.openid.connect.sdk.Nonce
import gitbucket.core.helper.xml import gitbucket.core.helper.xml
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.view.helpers.* import gitbucket.core.view.helpers._
import org.scalatra.Ok import org.scalatra.Ok
import org.scalatra.forms.* import org.scalatra.forms._
import gitbucket.core.service.ActivityService.*
class IndexController class IndexController
extends IndexControllerBase extends IndexControllerBase
@@ -34,12 +31,19 @@ class IndexController
with RequestCache with RequestCache
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService & ActivityService & AccountService & RepositorySearchService & UsersAuthenticator & self: RepositoryService
ReferrerAuthenticator & AccessTokenService & AccountFederationService & OpenIDConnectService => with ActivityService
with AccountService
with RepositorySearchService
with UsersAuthenticator
with ReferrerAuthenticator
with AccessTokenService
with AccountFederationService
with OpenIDConnectService =>
private case class SignInForm(userName: String, password: String, hash: Option[String]) case class SignInForm(userName: String, password: String, hash: Option[String])
private val signinForm = mapping( val signinForm = mapping(
"userName" -> trim(label("Username", text(required))), "userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required))), "password" -> trim(label("Password", text(required))),
"hash" -> trim(optional(text())) "hash" -> trim(optional(text()))
@@ -53,53 +57,38 @@ trait IndexControllerBase extends ControllerBase {
// //
// case class SearchForm(query: String, owner: String, repository: String) // case class SearchForm(query: String, owner: String, repository: String)
private case class OidcAuthContext(state: State, nonce: Nonce, redirectBackURI: String) case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String)
private case class OidcSessionContext(token: JWT)
get("/") { get("/") {
context.loginAccount context.loginAccount
.map { account => .map { account =>
// val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName) val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
if (!isNewsFeedEnabled) { gitbucket.core.html.index(
redirect("/dashboard/repos") getRecentActivitiesByOwners(visibleOwnerSet),
} else { getVisibleRepositories(
val repos = getVisibleRepositories(
Some(account), Some(account),
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = false limit = context.settings.basicBehavior.limitVisibleRepositories
) ),
gitbucket.core.html.index(
activities = getRecentActivitiesByRepos(repos.map(x => (x.owner, x.name)).toSet),
recentRepositories = if (context.settings.basicBehavior.limitVisibleRepositories) {
repos.filter(x => x.owner == account.userName)
} else repos,
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken( showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName account.userName
),
enableNewsFeed = isNewsFeedEnabled
) )
} )
} }
.getOrElse { .getOrElse {
gitbucket.core.html.index( gitbucket.core.html.index(
activities = getRecentPublicActivities(), getRecentPublicActivities(),
recentRepositories = getVisibleRepositories(None, withoutPhysicalInfo = true), getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false, showBannerToCreatePersonalAccessToken = false
enableNewsFeed = isNewsFeedEnabled
) )
} }
} }
get("/signin") { get("/signin") {
if (context.loginAccount.nonEmpty) { val redirect = params.get("redirect")
redirect("/") if (redirect.isDefined && redirect.get.startsWith("/")) {
} flash.update(Keys.Flash.Redirect, redirect.get)
params.get("redirect").foreach { redirect =>
if (redirect.startsWith("/")) {
flash.update(Keys.Flash.Redirect, redirect)
}
} }
gitbucket.core.html.signin(flash.get("userName"), flash.get("password"), flash.get("error")) gitbucket.core.html.signin(flash.get("userName"), flash.get("password"), flash.get("error"))
} }
@@ -131,8 +120,8 @@ trait IndexControllerBase extends ControllerBase {
case _ => "/" case _ => "/"
} }
session.setAttribute( session.setAttribute(
Keys.Session.OidcAuthContext, Keys.Session.OidcContext,
OidcAuthContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI) OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
) )
redirect(authenticationRequest.toURI.toString) redirect(authenticationRequest.toURI.toString)
} getOrElse { } getOrElse {
@@ -146,10 +135,9 @@ trait IndexControllerBase extends ControllerBase {
get("/signin/oidc") { get("/signin/oidc") {
context.settings.oidc.map { oidc => context.settings.oidc.map { oidc =>
val redirectURI = new URI(s"$baseUrl/signin/oidc") val redirectURI = new URI(s"$baseUrl/signin/oidc")
session.get(Keys.Session.OidcAuthContext) match { session.get(Keys.Session.OidcContext) match {
case Some(context: OidcAuthContext) => case Some(context: OidcContext) =>
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { case (jwt, account) => authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { account =>
session.setAttribute(Keys.Session.OidcSessionContext, OidcSessionContext(jwt))
signin(account, context.redirectBackURI) signin(account, context.redirectBackURI)
} orElse { } orElse {
flash.update("error", "Sorry, authentication failed. Please try again.") flash.update("error", "Sorry, authentication failed. Please try again.")
@@ -167,18 +155,7 @@ trait IndexControllerBase extends ControllerBase {
} }
get("/signout") { get("/signout") {
context.settings.oidc.foreach { oidc => session.invalidate
session.get(Keys.Session.OidcSessionContext).foreach { case context: OidcSessionContext =>
val redirectURI = new URI(baseUrl)
val authenticationRequest = createOIDLogoutRequest(oidc.issuer, oidc.clientID, redirectURI, context.token)
session.invalidate()
redirect(authenticationRequest.toURI.toString)
}
}
session.invalidate()
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
deleteLoginAccountFromLocalFile()
}
redirect("/") redirect("/")
} }
@@ -196,24 +173,11 @@ trait IndexControllerBase extends ControllerBase {
Ok() Ok()
} }
get("/user.css") {
context.settings.userDefinedCss match {
case Some(css) =>
contentType = "text/css"
css
case None =>
NotFound()
}
}
/** /**
* Set account information into HttpSession and redirect. * Set account information into HttpSession and redirect.
*/ */
private def signin(account: Account, redirectUrl: String = "/") = { private def signin(account: Account, redirectUrl: String = "/") = {
session.setAttribute(Keys.Session.LoginAccount, account) session.setAttribute(Keys.Session.LoginAccount, account)
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
saveLoginAccountToLocalFile(account)
}
updateLastLoginDate(account.userName) updateLastLoginDate(account.userName)
if (LDAPUtil.isDummyMailAddress(account)) { if (LDAPUtil.isDummyMailAddress(account)) {
@@ -236,8 +200,8 @@ trait IndexControllerBase extends ControllerBase {
val group = params("group").toBoolean val group = params("group").toBoolean
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map( Map(
"options" -> "options" -> (
getAllUsers(includeRemoved = false) getAllUsers(false)
.withFilter { t => .withFilter { t =>
(user, group) match { (user, group) match {
case (true, true) => true case (true, true) => true
@@ -257,35 +221,24 @@ trait IndexControllerBase extends ControllerBase {
} }
) )
) )
)
}) })
/** /**
* JSON API for checking user or group existence. * JSON API for checking user or group existence.
*
* Returns a single string which is any of "group", "user" or "". * 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 { post("/_user/existence")(usersOnly {
getAccountByUserNameIgnoreCase(params("userName")).map { account => getAccountByUserNameIgnoreCase(params("userName")).map { account =>
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" if (account.isGroupAccount) "group" else "user"
}
} getOrElse "" } getOrElse ""
}) })
// TODO Move to RepositoryViewerController? // TODO Move to RepositoryViewrController?
get("/:owner/:repository/search")(referrersOnly { repository => get("/:owner/:repository/search")(referrersOnly { repository =>
val query = params.getOrElse("q", "").trim val query = params.getOrElse("q", "").trim
val target = params.getOrElse("type", "code") val target = params.getOrElse("type", "code")
val page = val page = try {
try {
val i = params.getOrElse("page", "1").toInt val i = params.getOrElse("page", "1").toInt
if (i <= 0) 1 else i if (i <= 0) 1 else i
} catch { } catch {
@@ -295,8 +248,8 @@ trait IndexControllerBase extends ControllerBase {
target.toLowerCase match { target.toLowerCase match {
case "issues" => case "issues" =>
gitbucket.core.search.html.issues( gitbucket.core.search.html.issues(
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, pullRequest = false) else Nil, if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, false) else Nil,
pullRequest = false, false,
query, query,
page, page,
repository repository
@@ -304,8 +257,8 @@ trait IndexControllerBase extends ControllerBase {
case "pulls" => case "pulls" =>
gitbucket.core.search.html.issues( gitbucket.core.search.html.issues(
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, pullRequest = true) else Nil, if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, true) else Nil,
pullRequest = true, true,
query, query,
page, page,
repository repository
@@ -340,15 +293,15 @@ trait IndexControllerBase extends ControllerBase {
) )
val repositories = { val repositories = {
if (context.settings.basicBehavior.limitVisibleRepositories) { context.settings.basicBehavior.limitVisibleRepositories match {
case true =>
getVisibleRepositories( getVisibleRepositories(
context.loginAccount, context.loginAccount,
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = false limit = false
) )
} else { case false => visibleRepositories
visibleRepositories
} }
}.filter { repository => }.filter { repository =>
repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0 repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0

View File

@@ -2,13 +2,13 @@ package gitbucket.core.controller
import gitbucket.core.issues.html import gitbucket.core.issues.html
import gitbucket.core.model.{Account, CustomFieldBehavior} import gitbucket.core.model.{Account, CustomFieldBehavior}
import gitbucket.core.service.IssuesService.* import gitbucket.core.service.IssuesService._
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.Markdown import gitbucket.core.view.Markdown
import org.scalatra.forms.* import org.scalatra.forms._
import org.scalatra.{BadRequest, Ok} import org.scalatra.{BadRequest, Ok}
class IssuesController class IssuesController
@@ -34,12 +34,23 @@ class IssuesController
with RequestCache with RequestCache
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService & RepositoryService & AccountService & LabelsService & MilestonesService & ActivityService & self: IssuesService
HandleCommentService & IssueCreationService & CustomFieldsService & ReadableUsersAuthenticator & with RepositoryService
ReferrerAuthenticator & WritableUsersAuthenticator & PullRequestService & WebHookIssueCommentService & with AccountService
PrioritiesService => with LabelsService
with MilestonesService
with ActivityService
with HandleCommentService
with IssueCreationService
with CustomFieldsService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with PullRequestService
with WebHookIssueCommentService
with PrioritiesService =>
private case class IssueCreateForm( case class IssueCreateForm(
title: String, title: String,
content: Option[String], content: Option[String],
assigneeUserNames: Option[String], assigneeUserNames: Option[String],
@@ -47,10 +58,10 @@ trait IssuesControllerBase extends ControllerBase {
priorityId: Option[Int], priorityId: Option[Int],
labelNames: Option[String] labelNames: Option[String]
) )
private case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
private case class IssueStateForm(issueId: Int, content: Option[String]) case class IssueStateForm(issueId: Int, content: Option[String])
private val issueCreateForm = mapping( val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())), "content" -> trim(optional(text())),
"assigneeUserNames" -> trim(optional(text())), "assigneeUserNames" -> trim(optional(text())),
@@ -59,46 +70,38 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
private val issueTitleEditForm = mapping( val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required))) "title" -> trim(label("Title", text(required)))
)(x => x) )(x => x)
private val issueEditForm = mapping( val issueEditForm = mapping(
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(x => x) )(x => x)
private val commentForm = mapping( val commentForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(label("Comment", text(required))) "content" -> trim(label("Comment", text(required)))
)(CommentForm.apply) )(CommentForm.apply)
private val issueStateForm = mapping( val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { repository => get("/:owner/:repository/issues")(referrersOnly { repository =>
val q = request.getParameter("q") val q = request.getParameter("q")
Option(q) match { if (Option(q).exists(_.contains("is:pr"))) {
case Some(filter) if filter.contains("is:pr") =>
redirect(s"/${repository.owner}/${repository.name}/pulls?q=${StringUtil.urlEncode(q)}") redirect(s"/${repository.owner}/${repository.name}/pulls?q=${StringUtil.urlEncode(q)}")
case Some(filter) =>
val condition = IssueSearchCondition(filter)
if (condition.isEmpty) {
// Redirect to keyword search
redirect(s"/${repository.owner}/${repository.name}/search?q=${StringUtil.urlEncode(q)}&type=issues")
} else { } else {
searchIssues(repository, condition, IssueSearchCondition.page(request)) searchIssues(repository)
}
case None =>
searchIssues(repository, IssueSearchCondition(request), IssueSearchCondition.page(request))
} }
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
val issueId = params("id") val issueId = params("id")
getIssue(repository.owner, repository.name, issueId) map { issue => getIssue(repository.owner, repository.name, issueId) map {
issue =>
if (issue.isPullRequest) { if (issue.isPullRequest) {
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else { } else {
html.issue( html.issue(
issue, issue,
@@ -136,7 +139,8 @@ trait IssuesControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator? if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator?
val issue = createIssue( val issue = createIssue(
repository, repository,
@@ -150,7 +154,8 @@ trait IssuesControllerBase extends ControllerBase {
) )
// Insert custom field values // Insert custom field values
params.toMap.foreach { case (key, value) => params.toMap.foreach {
case (key, value) =>
if (key.startsWith("custom-field-")) { if (key.startsWith("custom-field-")) {
getCustomField( getCustomField(
repository.owner, repository.owner,
@@ -172,8 +177,10 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
getIssue(repository.owner, repository.name, params("id")).map { issue => loginAccount =>
getIssue(repository.owner, repository.name, params("id")).map {
issue =>
if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) { if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
if (issue.title != title) { if (issue.title != title) {
// update issue // update issue
@@ -196,7 +203,8 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, params("id")).map { issue => getIssue(repository.owner, repository.name, params("id")).map { issue =>
if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) { if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
// update issue // update issue
@@ -211,15 +219,17 @@ trait IssuesControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt = val actionOpt =
params params
.get("action") .get("action")
.filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount)) .filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) => handleComment(issue, Some(form.content), repository, actionOpt) map {
case (issue, id) =>
redirect( redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-$id" s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
) )
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -227,15 +237,17 @@ trait IssuesControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt = val actionOpt =
params params
.get("action") .get("action")
.filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount)) .filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) => handleComment(issue, form.content, repository, actionOpt) map {
case (issue, id) =>
redirect( redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-$id" s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
) )
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -243,7 +255,8 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
getComment(repository.owner, repository.name, params("id")).map { comment => getComment(repository.owner, repository.name, params("id")).map { comment =>
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) { if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content) updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content)
@@ -264,8 +277,10 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
getIssue(repository.owner, repository.name, params("id")) map { x => loginAccount =>
getIssue(repository.owner, repository.name, params("id")) map {
x =>
if (isEditableContent(x.userName, x.repositoryName, x.openedUserName, loginAccount)) { if (isEditableContent(x.userName, x.repositoryName, x.openedUserName, loginAccount)) {
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => html.editissue(x.content, x.issueId, repository) case t if t == "html" => html.editissue(x.content, x.issueId, repository)
@@ -294,8 +309,10 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
getComment(repository.owner, repository.name, params("id")) map { x => loginAccount =>
getComment(repository.owner, repository.name, params("id")) map {
x =>
if (isEditableContent(x.userName, x.repositoryName, x.commentedUserName, loginAccount)) { if (isEditableContent(x.userName, x.repositoryName, x.commentedUserName, loginAccount)) {
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => html.editcomment(x.content, x.commentId, repository) case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
@@ -330,40 +347,35 @@ trait IssuesControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository =>
val issueId = params("id").toInt val issueId = params("id").toInt
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, insertComment = true) registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}) })
ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository =>
val issueId = params("id").toInt val issueId = params("id").toInt
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, insertComment = true) deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}) })
ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
val issueId = params("id").toInt val issueId = params("id").toInt
registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), insertComment = true) registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
Ok() Ok()
}) })
ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
val issueId = params("id").toInt val issueId = params("id").toInt
deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), insertComment = true) deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
Ok() Ok()
}) })
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
updateMilestoneId( updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"), true)
repository.owner,
repository.name,
params("id").toInt,
milestoneId("milestoneId"),
insertComment = true
)
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId) .find(_._1.milestoneId == milestoneId)
.map { case (_, openCount, closeCount) => .map {
case (_, openCount, closeCount) =>
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount) gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound() } getOrElse NotFound()
} getOrElse Ok() } getOrElse Ok()
@@ -371,7 +383,7 @@ trait IssuesControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository =>
val priority = priorityId("priorityId") val priority = priorityId("priorityId")
updatePriorityId(repository.owner, repository.name, params("id").toInt, priority, insertComment = true) updatePriorityId(repository.owner, repository.name, params("id").toInt, priority, true)
Ok("updated") Ok("updated")
}) })
@@ -433,7 +445,7 @@ trait IssuesControllerBase extends ControllerBase {
params("value").toIntOpt.map { labelId => params("value").toIntOpt.map { labelId =>
executeBatch(repository) { issueId => executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId, insertComment = true) registerIssueLabel(repository.owner, repository.name, issueId, labelId, true)
if (params("uri").nonEmpty) { if (params("uri").nonEmpty) {
redirect(params("uri")) redirect(params("uri"))
} }
@@ -445,12 +457,12 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
val value = assignedUserName("value") val value = assignedUserName("value")
executeBatch(repository) { executeBatch(repository) {
// updateAssignedUserName(repository.owner, repository.name, _, value, true) //updateAssignedUserName(repository.owner, repository.name, _, value, true)
value match { value match {
case Some(assignedUserName) => case Some(assignedUserName) =>
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, insertComment = true) registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
case None => case None =>
deleteAllIssueAssignees(repository.owner, repository.name, _, insertComment = true) deleteAllIssueAssignees(repository.owner, repository.name, _, true)
} }
} }
if (params("uri").nonEmpty) { if (params("uri").nonEmpty) {
@@ -461,20 +473,20 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository =>
val value = milestoneId("value") val value = milestoneId("value")
executeBatch(repository) { executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value, insertComment = true) updateMilestoneId(repository.owner, repository.name, _, value, true)
} }
}) })
post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository =>
val value = priorityId("value") val value = priorityId("value")
executeBatch(repository) { executeBatch(repository) {
updatePriorityId(repository.owner, repository.name, _, value, insertComment = true) updatePriorityId(repository.owner, repository.name, _, value, true)
} }
}) })
get("/:owner/:repository/_attached/:file")(referrersOnly { repository => get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
(Directory.getAttachedDir(repository.owner, repository.name) match { (Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if dir.exists && dir.isDirectory => case dir if (dir.exists && dir.isDirectory) =>
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""") response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
RawData(FileUtil.getSafeMimeType(file.getName), file) RawData(FileUtil.getSafeMimeType(file.getName), file)
@@ -490,7 +502,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map( Map(
"options" -> "options" -> (
getOpenIssues(repository.owner, repository.name) getOpenIssues(repository.owner, repository.name)
.map { t => .map { t =>
Map( Map(
@@ -503,13 +515,14 @@ trait IssuesControllerBase extends ControllerBase {
} }
) )
) )
)
}) })
private val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
private val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit): Unit = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map (_.toInt) foreach execute params("checked").split(',') map (_.toInt) foreach execute
params("from") match { params("from") match {
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
@@ -518,7 +531,10 @@ trait IssuesControllerBase extends ControllerBase {
} }
} }
private def searchIssues(repository: RepositoryService.RepositoryInfo, condition: IssueSearchCondition, page: Int) = { private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
val page = IssueSearchCondition.page(request)
// retrieve search condition
val condition = IssueSearchCondition(request)
// search issues // search issues
val issues = val issues =
searchIssue( searchIssue(
@@ -549,8 +565,8 @@ trait IssuesControllerBase extends ControllerBase {
/** /**
* Tests whether an issue or a comment is editable by a logged-in user. * Tests whether an issue or a comment is editable by a logged-in user.
*/ */
private def isEditableContent(owner: String, repository: String, author: String, loginAccount: Account)(implicit private def isEditableContent(owner: String, repository: String, author: String, loginAccount: Account)(
context: Context implicit context: Context
): Boolean = { ): Boolean = {
hasDeveloperRole(owner, repository, context.loginAccount) || author == loginAccount.userName hasDeveloperRole(owner, repository, context.loginAccount) || author == loginAccount.userName
} }
@@ -558,8 +574,8 @@ trait IssuesControllerBase extends ControllerBase {
/** /**
* Tests whether an issue comment is deletable by a logged-in user. * Tests whether an issue comment is deletable by a logged-in user.
*/ */
private def isDeletableComment(owner: String, repository: String, author: String, loginAccount: Account)(implicit private def isDeletableComment(owner: String, repository: String, author: String, loginAccount: Account)(
context: Context implicit context: Context
): Boolean = { ): Boolean = {
hasOwnerRole(owner, repository, context.loginAccount) || author == loginAccount.userName hasOwnerRole(owner, repository, context.loginAccount) || author == loginAccount.userName
} }

View File

@@ -10,9 +10,9 @@ import gitbucket.core.service.{
PrioritiesService PrioritiesService
} }
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars.* import gitbucket.core.util.SyntaxSugars._
import org.scalatra.forms.* import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok import org.scalatra.Ok
@@ -28,11 +28,15 @@ class LabelsController
with WritableUsersAuthenticator with WritableUsersAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService & IssuesService & RepositoryService & ReferrerAuthenticator & WritableUsersAuthenticator => self: LabelsService
with IssuesService
with RepositoryService
with ReferrerAuthenticator
with WritableUsersAuthenticator =>
private case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
private val labelForm = mapping( val labelForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))), "labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color))) "labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
@@ -89,9 +93,9 @@ trait LabelsControllerBase extends ControllerBase {
private def labelName: Constraint = new Constraint() { private def labelName: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (value.contains(',')) { if (value.contains(',')) {
Some(s"$name contains invalid character.") Some(s"${name} contains invalid character.")
} else if (value.startsWith("_") || value.startsWith("-")) { } else if (value.startsWith("_") || value.startsWith("-")) {
Some(s"$name starts with invalid character.") Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }

View File

@@ -9,11 +9,11 @@ import gitbucket.core.service.{
MilestonesService, MilestonesService,
RepositoryService RepositoryService
} }
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.SyntaxSugars.* import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.view.helpers.{getAssignableUserNames, getLabels, getPriorities, searchIssue} import gitbucket.core.view.helpers.{getAssignableUserNames, getLabels, getPriorities, searchIssue}
import org.scalatra.forms.* import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
class MilestonesController class MilestonesController
@@ -26,12 +26,15 @@ class MilestonesController
with WritableUsersAuthenticator with WritableUsersAuthenticator
trait MilestonesControllerBase extends ControllerBase { trait MilestonesControllerBase extends ControllerBase {
self: MilestonesService & RepositoryService & CommitStatusService & ReferrerAuthenticator & self: MilestonesService
WritableUsersAuthenticator => with RepositoryService
with CommitStatusService
with ReferrerAuthenticator
with WritableUsersAuthenticator =>
private case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date]) case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date])
private val milestoneForm = mapping( val milestoneForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100), uniqueMilestone))), "title" -> trim(label("Title", text(required, maxlength(100), uniqueMilestone))),
"description" -> trim(label("Description", optional(text()))), "description" -> trim(label("Description", optional(text()))),
"dueDate" -> trim(label("Due Date", optional(date()))) "dueDate" -> trim(label("Due Date", optional(date())))

View File

@@ -30,14 +30,12 @@ trait PreProcessControllerBase extends ControllerBase {
* But if it's not allowed, demands authentication except some paths. * But if it's not allowed, demands authentication except some paths.
*/ */
get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) { get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) {
if ( if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") && !context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
!context.currentPath.startsWith("/plugin-assets") && !context.currentPath.equals("/user.css") && !context.currentPath.startsWith("/plugin-assets") &&
!PluginRegistry().getAnonymousAccessiblePaths().exists { path => !PluginRegistry().getAnonymousAccessiblePaths().exists { path =>
context.currentPath.startsWith(path) context.currentPath.startsWith(path)
} }) {
) {
Unauthorized() Unauthorized()
} else { } else {
pass() pass()

View File

@@ -28,11 +28,15 @@ class PrioritiesController
with WritableUsersAuthenticator with WritableUsersAuthenticator
trait PrioritiesControllerBase extends ControllerBase { trait PrioritiesControllerBase extends ControllerBase {
self: PrioritiesService & IssuesService & RepositoryService & ReferrerAuthenticator & WritableUsersAuthenticator => self: PrioritiesService
with IssuesService
with RepositoryService
with ReferrerAuthenticator
with WritableUsersAuthenticator =>
private case class PriorityForm(priorityName: String, description: Option[String], color: String) case class PriorityForm(priorityName: String, description: Option[String], color: String)
private val priorityForm = mapping( val priorityForm = mapping(
"priorityName" -> trim(label("Priority name", text(required, priorityName, uniquePriorityName, maxlength(100)))), "priorityName" -> trim(label("Priority name", text(required, priorityName, uniquePriorityName, maxlength(100)))),
"description" -> trim(label("Description", optional(text(maxlength(255))))), "description" -> trim(label("Description", optional(text(maxlength(255))))),
"priorityColor" -> trim(label("Color", text(required, color))) "priorityColor" -> trim(label("Color", text(required, color)))
@@ -86,7 +90,7 @@ trait PrioritiesControllerBase extends ControllerBase {
) )
}) })
ajaxPost("/:owner/:repository/issues/priorities/reorder")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/priorities/reorder")(writableUsersOnly { (repository) =>
reorderPriorities( reorderPriorities(
repository.owner, repository.owner,
repository.name, repository.name,
@@ -100,7 +104,7 @@ trait PrioritiesControllerBase extends ControllerBase {
Ok() Ok()
}) })
ajaxPost("/:owner/:repository/issues/priorities/default")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/priorities/default")(writableUsersOnly { (repository) =>
setDefaultPriority(repository.owner, repository.name, priorityId("priorityId")) setDefaultPriority(repository.owner, repository.name, priorityId("priorityId"))
Ok() Ok()
}) })
@@ -118,9 +122,9 @@ trait PrioritiesControllerBase extends ControllerBase {
private def priorityName: Constraint = new Constraint() { private def priorityName: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (value.contains(',')) { if (value.contains(',')) {
Some(s"$name contains invalid character.") Some(s"${name} contains invalid character.")
} else if (value.startsWith("_") || value.startsWith("-")) { } else if (value.startsWith("_") || value.startsWith("-")) {
Some(s"$name starts with invalid character.") Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }

View File

@@ -4,19 +4,17 @@ import gitbucket.core.model.activity.DeleteBranchInfo
import gitbucket.core.pulls.html import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService import gitbucket.core.service.MergeService
import gitbucket.core.service.IssuesService.* import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService.* import gitbucket.core.service.PullRequestService._
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits.* 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
@@ -42,19 +40,32 @@ class PullRequestsController
with RequestCache with RequestCache
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService & AccountService & IssuesService & MilestonesService & LabelsService & CustomFieldsService & self: RepositoryService
CommitsService & ActivityService & PullRequestService & WebHookPullRequestService & ReadableUsersAuthenticator & with AccountService
ReferrerAuthenticator & WritableUsersAuthenticator & CommitStatusService & MergeService & ProtectedBranchService & with IssuesService
PrioritiesService => with MilestonesService
with LabelsService
with CustomFieldsService
with CommitsService
with ActivityService
with PullRequestService
with WebHookPullRequestService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with CommitStatusService
with MergeService
with ProtectedBranchService
with PrioritiesService =>
private val pullRequestForm = mapping( val pullRequestForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(label("Content", optional(text()))), "content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))), "targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(255))), "targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))), "requestUserName" -> trim(text(required, maxlength(100))),
"requestRepositoryName" -> trim(text(required, maxlength(100))), "requestRepositoryName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(255))), "requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))), "commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40))), "commitIdTo" -> trim(text(required, maxlength(40))),
"isDraft" -> trim(boolean(required)), "isDraft" -> trim(boolean(required)),
@@ -64,13 +75,13 @@ trait PullRequestsControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(PullRequestForm.apply) )(PullRequestForm.apply)
private val mergeForm = mapping( val mergeForm = mapping(
"message" -> trim(label("Message", text(required))), "message" -> trim(label("Message", text(required))),
"strategy" -> trim(label("Strategy", text(required))), "strategy" -> trim(label("Strategy", text(required))),
"isDraft" -> trim(boolean(required)) "isDraft" -> trim(boolean(required))
)(MergeForm.apply) )(MergeForm.apply)
private case class PullRequestForm( case class PullRequestForm(
title: String, title: String,
content: Option[String], content: Option[String],
targetUserName: String, targetUserName: String,
@@ -87,29 +98,22 @@ trait PullRequestsControllerBase extends ControllerBase {
labelNames: Option[String] labelNames: Option[String]
) )
private case class MergeForm(message: String, strategy: String, isDraft: Boolean) case class MergeForm(message: String, strategy: String, isDraft: Boolean)
get("/:owner/:repository/pulls")(referrersOnly { repository => get("/:owner/:repository/pulls")(referrersOnly { repository =>
val q = request.getParameter("q") val q = request.getParameter("q")
Option(q) match { if (Option(q).exists(_.contains("is:issue"))) {
case Some(filter) if filter.contains("is:issue") => redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
redirect(s"/${repository.owner}/${repository.name}/issues?q=${StringUtil.urlEncode(q)}")
case Some(filter) =>
val condition = IssueSearchCondition(filter)
if (condition.isEmpty) {
// Redirect to keyword search
redirect(s"/${repository.owner}/${repository.name}/search?q=${StringUtil.urlEncode(q)}&type=pulls")
} else { } else {
searchPullRequests(repository, IssueSearchCondition(filter), IssueSearchCondition.page(request)) searchPullRequests(None, repository)
}
case None =>
searchPullRequests(repository, IssueSearchCondition(request), IssueSearchCondition.page(request))
} }
}) })
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap {
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) => issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) = val (commits, diffs) =
getRequestCompareInfo( getRequestCompareInfo(
repository.owner, repository.owner,
@@ -117,8 +121,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq.commitIdFrom, pullreq.commitIdFrom,
repository.owner, repository.owner,
repository.name, repository.name,
pullreq.commitIdTo, pullreq.commitIdTo
context.settings
) )
html.conversation( html.conversation(
@@ -146,8 +149,10 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/pull/:id/commits")(referrersOnly { repository => get("/:owner/:repository/pull/:id/commits")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap {
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) => issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) = val (commits, diffs) =
getRequestCompareInfo( getRequestCompareInfo(
repository.owner, repository.owner,
@@ -155,8 +160,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq.commitIdFrom, pullreq.commitIdFrom,
repository.owner, repository.owner,
repository.name, repository.name,
pullreq.commitIdTo, pullreq.commitIdTo
context.settings
) )
val commitsWithStatus = commits.map { day => val commitsWithStatus = commits.map { day =>
@@ -179,8 +183,10 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/pull/:id/files")(referrersOnly { repository => get("/:owner/:repository/pull/:id/files")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap {
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) => issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) = val (commits, diffs) =
getRequestCompareInfo( getRequestCompareInfo(
repository.owner, repository.owner,
@@ -188,8 +194,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq.commitIdFrom, pullreq.commitIdFrom,
repository.owner, repository.owner,
repository.name, repository.name,
pullreq.commitIdTo, pullreq.commitIdTo
context.settings
) )
html.files( html.files(
@@ -206,8 +211,10 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository => ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap {
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) => issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val conflictMessage = LockUtil.lock(s"${repository.owner}/${repository.name}") { val conflictMessage = LockUtil.lock(s"${repository.owner}/${repository.name}") {
checkConflict(repository.owner, repository.name, pullreq.branch, issueId) checkConflict(repository.owner, repository.name, pullreq.branch, issueId)
} }
@@ -217,8 +224,9 @@ trait PullRequestsControllerBase extends ControllerBase {
conflictMessage = conflictMessage, conflictMessage = conflictMessage,
commitStatuses = getCommitStatuses(repository.owner, repository.name, pullreq.commitIdTo), commitStatuses = getCommitStatuses(repository.owner, repository.name, pullreq.commitIdTo),
branchProtection = branchProtection, branchProtection = branchProtection,
branchIsOutOfDate = branchIsOutOfDate = JGitUtil.getShaByRef(repository.owner, repository.name, pullreq.branch) != Some(
!JGitUtil.getShaByRef(repository.owner, repository.name, pullreq.branch).contains(pullreq.commitIdFrom), pullreq.commitIdFrom
),
needStatusCheck = context.loginAccount.forall { u => needStatusCheck = context.loginAccount.forall { u =>
branchProtection.needStatusCheck(u.userName) branchProtection.needStatusCheck(u.userName)
}, },
@@ -249,10 +257,10 @@ 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
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId) loginAccount <- context.loginAccount
(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)
@@ -266,8 +274,7 @@ trait PullRequestsControllerBase extends ControllerBase {
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( createComment(
@@ -285,14 +292,13 @@ trait PullRequestsControllerBase extends ControllerBase {
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 =>
(for { (for {
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount loginAccount <- context.loginAccount
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId) (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
repository <- getRepository(pullreq.requestUserName, pullreq.requestRepositoryName) repository <- getRepository(pullreq.requestUserName, pullreq.requestRepositoryName)
remoteRepository <- getRepository(pullreq.userName, pullreq.repositoryName) remoteRepository <- getRepository(pullreq.userName, pullreq.repositoryName)
owner = pullreq.requestUserName owner = pullreq.requestUserName
@@ -303,36 +309,34 @@ trait PullRequestsControllerBase extends ControllerBase {
if (branchProtection.needStatusCheck(loginAccount.userName)) { if (branchProtection.needStatusCheck(loginAccount.userName)) {
flash.update("error", s"branch ${pullreq.requestBranch} is protected need status check.") flash.update("error", s"branch ${pullreq.requestBranch} is protected need status check.")
} else { } else {
LockUtil.lock(s"$owner/$name") { LockUtil.lock(s"${owner}/${name}") {
val alias = val alias =
if ( if (pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName) {
pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName
) {
pullreq.branch pullreq.branch
} else { } else {
s"${pullreq.userName}:${pullreq.branch}" s"${pullreq.userName}:${pullreq.branch}"
} }
// val existIds = Using val existIds = Using
// .resource(Git.open(Directory.getRepositoryDir(owner, name))) { git => .resource(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
// JGitUtil.getAllCommitIds(git) JGitUtil.getAllCommitIds(git)
// } }
// .toSet .toSet
pullRemote( pullRemote(
repository, repository,
pullreq.requestBranch, pullreq.requestBranch,
remoteRepository, remoteRepository,
pullreq.branch, pullreq.branch,
loginAccount, loginAccount,
s"Merge branch '$alias' into ${pullreq.requestBranch}", s"Merge branch '${alias}' into ${pullreq.requestBranch}",
Some(pullreq), Some(pullreq),
context.settings context.settings
) match { ) match {
case None => // conflict case None => // conflict
flash.update("error", s"Can't automatic merging branch '$alias' into ${pullreq.requestBranch}.") flash.update("error", s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}.")
case Some(oldId) => case Some(oldId) =>
// update pull request // update pull request
updatePullRequests(owner, name, pullreq.requestBranch, loginAccount, "synchronize", context.settings) updatePullRequests(owner, name, pullreq.requestBranch, loginAccount, "synchronize", context.settings)
flash.update("info", s"Merge branch '$alias' into ${pullreq.requestBranch}") flash.update("info", s"Merge branch '${alias}' into ${pullreq.requestBranch}")
} }
} }
} }
@@ -344,7 +348,7 @@ trait PullRequestsControllerBase extends ControllerBase {
post("/:owner/:repository/pull/:id/update_draft")(readableUsersOnly { baseRepository => post("/:owner/:repository/pull/:id/update_draft")(readableUsersOnly { baseRepository =>
(for { (for {
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
case (_, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId) (_, 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)
@@ -365,11 +369,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()
} }
@@ -379,7 +380,8 @@ trait PullRequestsControllerBase extends ControllerBase {
val headBranch = params.get("head") val headBranch = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => case (Some(originUserName), Some(originRepositoryName)) =>
getRepository(originUserName, originRepositoryName).map { originRepository => getRepository(originUserName, originRepositoryName).map {
originRepository =>
Using.resources( Using.resources(
Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
@@ -390,15 +392,16 @@ trait PullRequestsControllerBase extends ControllerBase {
.getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2) .getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
redirect( redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/$originUserName:$oldBranch...$newBranch" s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}"
) )
} }
} getOrElse NotFound() } getOrElse NotFound()
case _ => case _ =>
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git => Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) => JGitUtil.getDefaultBranch(git, forkedRepository).map {
case (_, defaultBranch) =>
redirect( redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/$defaultBranch...${headBranch.getOrElse(defaultBranch)}" s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}"
) )
} getOrElse { } getOrElse {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}") redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
@@ -420,7 +423,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name) getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner) .find(_.userName == originOwner)
.map(_.repositoryName) .map(_.repositoryName)
} else if (forkedRepository.repository.originUserName.contains(originOwner)) { } else if (Some(originOwner) == forkedRepository.repository.originUserName) {
// Original repository // Original repository
forkedRepository.repository.originRepositoryName forkedRepository.repository.originRepositoryName
} else { } else {
@@ -439,10 +442,8 @@ trait PullRequestsControllerBase extends ControllerBase {
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner) val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for ( (for (originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository); originRepository <- getRepository(originOwner, originRepositoryName)) yield {
originRepository <- getRepository(originOwner, originRepositoryName)
) yield {
val (oldId, newId) = val (oldId, newId) =
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId) getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
@@ -454,8 +455,7 @@ trait PullRequestsControllerBase extends ControllerBase {
oldId.getName, oldId.getName,
forkedRepository.owner, forkedRepository.owner,
forkedRepository.name, forkedRepository.name,
newId.getName, newId.getName
context.settings
) )
val title = if (commits.flatten.length == 1) { val title = if (commits.flatten.length == 1) {
@@ -481,9 +481,7 @@ trait PullRequestsControllerBase extends ControllerBase {
(repository.userName, repository.repositoryName, repository.defaultBranch) (repository.userName, repository.repositoryName, repository.defaultBranch)
}, },
commits.flatten commits.flatten
.flatMap(commit => .flatMap(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false))
getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, includePullRequest = false)
)
.toList, .toList,
originId, originId,
forkedId, forkedId,
@@ -504,8 +502,8 @@ trait PullRequestsControllerBase extends ControllerBase {
case (oldId, newId) => case (oldId, newId) =>
redirect( redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" + s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
s"$originOwner:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." + s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
s"$forkedOwner:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}" s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}"
) )
} }
@@ -565,9 +563,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner)
(for { (for (originRepositoryName <- if (originOwner == forkedOwner) {
originRepositoryName <-
if (originOwner == forkedOwner) {
Some(forkedRepository.name) Some(forkedRepository.name)
} else { } else {
forkedRepository.repository.originRepositoryName.orElse { forkedRepository.repository.originRepositoryName.orElse {
@@ -575,13 +571,13 @@ trait PullRequestsControllerBase extends ControllerBase {
.find(_.userName == originOwner) .find(_.userName == originOwner)
.map(_.repositoryName) .map(_.repositoryName)
} }
} };
originRepository <- getRepository(originOwner, originRepositoryName) originRepository <- getRepository(originOwner, originRepositoryName)) yield {
} yield {
Using.resources( Using.resources(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
) { case (oldGit, newGit) => ) {
case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}") { val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}") {
@@ -600,7 +596,8 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val manageable = isManageable(repository) val manageable = isManageable(repository)
val issueId = insertIssue( val issueId = insertIssue(
@@ -646,7 +643,7 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
} }
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} }
}) })
@@ -656,7 +653,8 @@ trait PullRequestsControllerBase extends ControllerBase {
context.loginAccount.map(x => Seq(x.mailAddress) ++ getAccountExtraMailAddresses(x.userName)).getOrElse(Nil) context.loginAccount.map(x => Seq(x.mailAddress) ++ getAccountExtraMailAddresses(x.userName)).getOrElse(Nil)
val branches = val branches =
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
JGitUtil JGitUtil
.getBranches( .getBranches(
git = git, git = git,
@@ -692,11 +690,10 @@ trait PullRequestsControllerBase extends ControllerBase {
html.proposals(proposedBranches, targetRepository, repository) html.proposals(proposedBranches, targetRepository, repository)
}) })
private def searchPullRequests( private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = {
repository: RepositoryService.RepositoryInfo, val page = IssueSearchCondition.page(request)
condition: IssueSearchCondition, // retrieve search condition
page: Int val condition = IssueSearchCondition(request)
) = {
// search issues // search issues
val issues = searchIssue( val issues = searchIssue(
condition, condition,
@@ -729,107 +726,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 +744,5 @@ trait PullRequestsControllerBase extends ControllerBase {
case "DISABLE" => false case "DISABLE" => false
} }
} }
} }

View File

@@ -11,10 +11,10 @@ import gitbucket.core.service.{
RepositoryService, RepositoryService,
RequestCache RequestCache
} }
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import org.scalatra.forms.* import org.scalatra.forms._
import gitbucket.core.releases.html import gitbucket.core.releases.html
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@@ -33,15 +33,20 @@ class ReleaseController
with RequestCache with RequestCache
trait ReleaseControllerBase extends ControllerBase { trait ReleaseControllerBase extends ControllerBase {
self: RepositoryService & AccountService & ReleaseService & ReadableUsersAuthenticator & ReferrerAuthenticator & self: RepositoryService
WritableUsersAuthenticator & ActivityService => with AccountService
with ReleaseService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with ActivityService =>
private case class ReleaseForm( case class ReleaseForm(
name: String, name: String,
content: Option[String] content: Option[String]
) )
private val releaseForm = mapping( val releaseForm = mapping(
"name" -> trim(text(required)), "name" -> trim(text(required)),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(ReleaseForm.apply) )(ReleaseForm.apply)
@@ -101,7 +106,8 @@ trait ReleaseControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/releases/*/create", releaseForm)(writableUsersOnly { (form, repository) => post("/:owner/:repository/releases/*/create", releaseForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val tagName = multiParams("splat").head val tagName = multiParams("splat").head
// Insert into RELEASE // Insert into RELEASE
@@ -113,7 +119,8 @@ trait ReleaseControllerBase extends ControllerBase {
val Array(_, fileId) = name.split(":") val Array(_, fileId) = name.split(":")
(fileId, value) (fileId, value)
} }
files.foreach { case (fileId, fileName) => files.foreach {
case (fileId, fileName) =>
val size = val size =
new File( new File(
getReleaseFilesDir(repository.owner, repository.name), getReleaseFilesDir(repository.owner, repository.name),
@@ -125,15 +132,16 @@ trait ReleaseControllerBase extends ControllerBase {
val releaseInfo = ReleaseInfo(repository.owner, repository.name, loginAccount.userName, form.name, tagName) val releaseInfo = ReleaseInfo(repository.owner, repository.name, loginAccount.userName, form.name, tagName)
recordActivity(releaseInfo) recordActivity(releaseInfo)
redirect(s"/${repository.owner}/${repository.name}/releases/$tagName") redirect(s"/${repository.owner}/${repository.name}/releases/${tagName}")
} }
}) })
get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository => get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository =>
val commitLog = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val Seq(previousTag, currentTag) = multiParams("splat") val Seq(previousTag, currentTag) = multiParams("splat")
val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.commitId }.getOrElse("")
val commits = JGitUtil.getCommitLog(git, previousTag, currentTag).reverse val commitLog = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val commits = JGitUtil.getCommitLog(git, previousTagId, currentTag).reverse
commits commits
.map { commit => .map { commit =>
s"- ${commit.shortMessage} ${commit.id}" s"- ${commit.shortMessage} ${commit.id}"
@@ -163,11 +171,13 @@ trait ReleaseControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/releases/*/edit", releaseForm)(writableUsersOnly { (form, repository) => post("/:owner/:repository/releases/*/edit", releaseForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val tagName = multiParams("splat").head val tagName = multiParams("splat").head
getRelease(repository.owner, repository.name, tagName) getRelease(repository.owner, repository.name, tagName)
.map { release => .map {
release =>
// Update RELEASE // Update RELEASE
updateRelease(repository.owner, repository.name, tagName, form.name, form.content) updateRelease(repository.owner, repository.name, tagName, form.name, form.content)
@@ -180,7 +190,8 @@ trait ReleaseControllerBase extends ControllerBase {
val Array(_, fileId) = name.split(":") val Array(_, fileId) = name.split(":")
(fileId, value) (fileId, value)
} }
files.foreach { case (fileId, fileName) => files.foreach {
case (fileId, fileName) =>
val size = val size =
new File( new File(
getReleaseFilesDir(repository.owner, repository.name), getReleaseFilesDir(repository.owner, repository.name),
@@ -199,7 +210,7 @@ trait ReleaseControllerBase extends ControllerBase {
} }
} }
redirect(s"/${release.userName}/${release.repositoryName}/releases/$tagName") redirect(s"/${release.userName}/${release.repositoryName}/releases/${tagName}")
} }
.getOrElse(NotFound()) .getOrElse(NotFound())
} }
@@ -217,7 +228,7 @@ trait ReleaseControllerBase extends ControllerBase {
}) })
private def fetchReleases(repository: RepositoryService.RepositoryInfo, page: Int) = { private def fetchReleases(repository: RepositoryService.RepositoryInfo, page: Int) = {
import gitbucket.core.service.ReleaseService.* import gitbucket.core.service.ReleaseService._
val (offset, limit) = ((page - 1) * ReleaseLimit, ReleaseLimit) val (offset, limit) = ((page - 1) * ReleaseLimit, ReleaseLimit)
val tagsToDisplay = repository.tags.reverse.slice(offset, offset + limit) val tagsToDisplay = repository.tags.reverse.slice(offset, offset + limit)
@@ -226,12 +237,9 @@ trait ReleaseControllerBase extends ControllerBase {
val assets = getReleaseAssetsMap(repository.owner, repository.name, releases) val assets = getReleaseAssetsMap(repository.owner, repository.name, releases)
val tagsWithReleases = tagsToDisplay.map { tag => val tagsWithReleases = tagsToDisplay.map { tag =>
( (tag, releases.find(_.tag == tag.name).map { release =>
tag,
releases.find(_.tag == tag.name).map { release =>
(release, assets(release)) (release, assets(release))
} })
)
} }
tagsWithReleases tagsWithReleases
} }

View File

@@ -4,16 +4,16 @@ import java.time.{LocalDateTime, ZoneOffset}
import java.util.Date import java.util.Date
import gitbucket.core.settings.html import gitbucket.core.settings.html
import gitbucket.core.model.{RepositoryWebHook, WebHook} import gitbucket.core.model.{RepositoryWebHook, WebHook}
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.service.WebHookService.* import gitbucket.core.service.WebHookService._
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.util.JGitUtil.* import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars.* import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory._
import gitbucket.core.model.WebHookContentType import gitbucket.core.model.WebHookContentType
import gitbucket.core.model.activity.RenameRepositoryInfo import gitbucket.core.model.activity.RenameRepositoryInfo
import org.scalatra.forms.* import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
@@ -37,11 +37,19 @@ class RepositorySettingsController
with RequestCache with RequestCache
trait RepositorySettingsControllerBase extends ControllerBase { trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService & AccountService & WebHookService & ProtectedBranchService & CommitStatusService & self: RepositoryService
DeployKeyService & CustomFieldsService & ActivityService & OwnerAuthenticator & UsersAuthenticator => with AccountService
with WebHookService
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with CustomFieldsService
with ActivityService
with OwnerAuthenticator
with UsersAuthenticator =>
// for repository options // for repository options
private case class OptionsForm( case class OptionsForm(
description: Option[String], description: Option[String],
isPrivate: Boolean, isPrivate: Boolean,
issuesOption: String, issuesOption: String,
@@ -54,7 +62,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
safeMode: Boolean safeMode: Boolean
) )
private val optionsForm = mapping( val optionsForm = mapping(
"description" -> trim(label("Description", optional(text()))), "description" -> trim(label("Description", optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())), "isPrivate" -> trim(label("Repository Type", boolean())),
"issuesOption" -> trim(label("Issues Option", text(required, featureOption))), "issuesOption" -> trim(label("Issues Option", text(required, featureOption))),
@@ -72,30 +80,25 @@ trait RepositorySettingsControllerBase extends ControllerBase {
} }
// for default branch // for default branch
private case class DefaultBranchForm(defaultBranch: String) case class DefaultBranchForm(defaultBranch: String)
private val defaultBranchForm = mapping( val defaultBranchForm = mapping(
"defaultBranch" -> trim(label("Default Branch", text(required, maxlength(100)))) "defaultBranch" -> trim(label("Default Branch", text(required, maxlength(100))))
)(DefaultBranchForm.apply) )(DefaultBranchForm.apply)
// for deploy key // for deploy key
private case class DeployKeyForm(title: String, publicKey: String, allowWrite: Boolean) case class DeployKeyForm(title: String, publicKey: String, allowWrite: Boolean)
private val deployKeyForm = mapping( val deployKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))), "title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim2(label("Key", text(required))), // TODO duplication check in the repository? "publicKey" -> trim2(label("Key", text(required))), // TODO duplication check in the repository?
"allowWrite" -> trim(label("Key", boolean())) "allowWrite" -> trim(label("Key", boolean()))
)(DeployKeyForm.apply) )(DeployKeyForm.apply)
// for web hook url addition // for web hook url addition
private case class WebHookForm( case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
url: String,
events: Set[WebHook.Event],
ctype: WebHookContentType,
token: Option[String]
)
private def webHookForm(update: Boolean) = def webHookForm(update: Boolean) =
mapping( mapping(
"url" -> trim(label("url", text(required, webHook(update)))), "url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents, "events" -> webhookEvents,
@@ -104,34 +107,32 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)((url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)) )((url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token))
// for rename repository // for rename repository
private case class RenameRepositoryForm(repositoryName: String) case class RenameRepositoryForm(repositoryName: String)
private val renameForm = mapping( val renameForm = mapping(
"repositoryName" -> trim( "repositoryName" -> trim(
label("New repository name", text(required, maxlength(100), repository, renameRepositoryName)) label("New repository name", text(required, maxlength(100), repository, renameRepositoryName))
) )
)(RenameRepositoryForm.apply) )(RenameRepositoryForm.apply)
// for transfer ownership // for transfer ownership
private case class TransferOwnerShipForm(newOwner: String) case class TransferOwnerShipForm(newOwner: String)
private val transferForm = mapping( val transferForm = mapping(
"newOwner" -> trim(label("New owner", text(required, transferUser))) "newOwner" -> trim(label("New owner", text(required, transferUser)))
)(TransferOwnerShipForm.apply) )(TransferOwnerShipForm.apply)
// for custom field // for custom field
private case class CustomFieldForm( case class CustomFieldForm(
fieldName: String, fieldName: String,
fieldType: String, fieldType: String,
constraints: Option[String],
enableForIssues: Boolean, enableForIssues: Boolean,
enableForPullRequests: Boolean enableForPullRequests: Boolean
) )
private val customFieldForm = mapping( val customFieldForm = mapping(
"fieldName" -> trim(label("Field name", text(required, maxlength(100)))), "fieldName" -> trim(label("Field name", text(required, maxlength(100)))),
"fieldType" -> trim(label("Field type", text(required))), "fieldType" -> trim(label("Field type", text(required))),
"constraints" -> trim(label("Constraints", optional(text()))),
"enableForIssues" -> trim(label("Enable for issues", boolean(required))), "enableForIssues" -> trim(label("Enable for issues", boolean(required))),
"enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))), "enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))),
)(CustomFieldForm.apply) )(CustomFieldForm.apply)
@@ -183,7 +184,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** Update default branch */ /** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
if (!repository.branchList.contains(form.defaultBranch)) { if (!repository.branchList.contains(form.defaultBranch)) {
redirect(s"/${repository.owner}/${repository.name}/settings/branches") redirect(s"/${repository.owner}/${repository.name}/settings/options")
} else { } else {
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch) saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
// Change repository HEAD // Change repository HEAD
@@ -197,13 +198,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** Branch protection for branch */ /** Branch protection for branch */
get("/:owner/:repository/settings/branches/*")(ownerOnly { repository => get("/:owner/:repository/settings/branches/*")(ownerOnly { repository =>
import gitbucket.core.api.* import gitbucket.core.api._
val branch = params("splat") val branch = params("splat")
if (!repository.branchList.contains(branch)) { if (!repository.branchList.contains(branch)) {
redirect(s"/${repository.owner}/${repository.name}/settings/branches") redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else { } else {
val protection = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch)) val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatusContexts( val lastWeeks = getRecentStatusContexts(
repository.owner, repository.owner,
repository.name, repository.name,
@@ -253,7 +254,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
ctype = WebHookContentType.FORM, ctype = WebHookContentType.FORM,
token = None token = None
) )
html.edithook(webhook, Set(WebHook.Push), repository, create = true) html.edithook(webhook, Set(WebHook.Push), repository, true)
}) })
/** /**
@@ -283,10 +284,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
Array(h.getName, h.getValue) Array(h.getName, h.getValue)
} }
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
import scala.concurrent.duration.* git =>
import scala.concurrent.* import scala.concurrent.duration._
import scala.jdk.CollectionConverters.* import scala.concurrent._
import scala.jdk.CollectionConverters._
import scala.util.control.NonFatal import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@@ -332,10 +334,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head
val toErrorMap: PartialFunction[Throwable, Map[String, String]] = { val toErrorMap: PartialFunction[Throwable, Map[String, String]] = {
case e: java.net.UnknownHostException => Map("error" -> s"Unknown host ${e.getMessage}") case e: java.net.UnknownHostException => Map("error" -> ("Unknown host " + e.getMessage))
case _: java.lang.IllegalArgumentException => Map("error" -> "invalid url") case e: java.lang.IllegalArgumentException => Map("error" -> ("invalid url"))
case _: org.apache.http.client.ClientProtocolException => Map("error" -> "invalid url") case e: org.apache.http.client.ClientProtocolException => Map("error" -> ("invalid url"))
case NonFatal(e) => Map("error" -> s"${e.getClass} ${e.getMessage}") case NonFatal(e) => Map("error" -> (s"${e.getClass} ${e.getMessage}"))
} }
contentType = formats("json") contentType = formats("json")
@@ -344,7 +346,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"url" -> url, "url" -> url,
"request" -> Await.result( "request" -> Await.result(
reqFuture reqFuture
.map(req => .map(
req =>
Map( Map(
"headers" -> _headers(req.getAllHeaders), "headers" -> _headers(req.getAllHeaders),
"payload" -> json "payload" -> json
@@ -355,11 +358,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
), ),
"response" -> Await.result( "response" -> Await.result(
resFuture resFuture
.map(res => .map(
res =>
Map( Map(
"status" -> res.getStatusLine.getStatusCode, "status" -> res.getStatusLine.getStatusCode,
"body" -> EntityUtils.toString(res.getEntity), "body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders) "headers" -> _headers(res.getAllHeaders())
) )
) )
.recover(toErrorMap), .recover(toErrorMap),
@@ -374,8 +378,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook edit page. * Display the web hook edit page.
*/ */
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map { case (webhook, events) => getWebHook(repository.owner, repository.name, params("url")).map {
html.edithook(webhook, events, repository, create = false) case (webhook, events) =>
html.edithook(webhook, events, repository, false)
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -399,7 +404,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Rename repository. * Rename repository.
*/ */
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) { if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
if (repository.name != form.repositoryName) { if (repository.name != form.repositoryName) {
// Update database and move git repository // Update database and move git repository
@@ -422,7 +428,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Transfer repository ownership. * Transfer repository ownership.
*/ */
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) { if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) {
// Change repository owner // Change repository owner
if (repository.owner != form.newOwner) { if (repository.owner != form.newOwner) {
@@ -504,7 +511,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository.name, repository.name,
form.fieldName, form.fieldName,
form.fieldType, form.fieldType,
if (form.fieldType == "enum") form.constraints else None,
form.enableForIssues, form.enableForIssues,
form.enableForPullRequests form.enableForPullRequests
) )
@@ -527,7 +533,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
params("fieldId").toInt, params("fieldId").toInt,
form.fieldName, form.fieldName,
form.fieldType, form.fieldType,
if (form.fieldType == "enum") form.constraints else None,
form.enableForIssues, form.enableForIssues,
form.enableForPullRequests form.enableForPullRequests
) )

View File

@@ -6,12 +6,12 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.repo.html import gitbucket.core.repo.html
import gitbucket.core.helper import gitbucket.core.helper
import gitbucket.core.model.activity.DeleteBranchInfo import gitbucket.core.model.activity.DeleteBranchInfo
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.service.RepositoryCommitFileService.CommitFile import gitbucket.core.service.RepositoryCommitFileService.CommitFile
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.util.StringUtil.* import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, WebHook} import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.WebHookService.{WebHookCreatePayload, WebHookPushPayload} import gitbucket.core.service.WebHookService.{WebHookCreatePayload, WebHookPushPayload}
@@ -25,11 +25,11 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream
import org.apache.commons.compress.utils.IOUtils import org.apache.commons.compress.utils.IOUtils
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.forms.* import org.scalatra.forms._
import org.eclipse.jgit.api.{ArchiveCommand, Git} import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.errors.MissingObjectException
import org.eclipse.jgit.lib.* import org.eclipse.jgit.lib._
import org.eclipse.jgit.treewalk.{TreeWalk, WorkingTreeOptions} import org.eclipse.jgit.treewalk.{TreeWalk, WorkingTreeOptions}
import org.eclipse.jgit.treewalk.TreeWalk.OperationType import org.eclipse.jgit.treewalk.TreeWalk.OperationType
import org.eclipse.jgit.treewalk.filter.PathFilter import org.eclipse.jgit.treewalk.filter.PathFilter
@@ -65,15 +65,26 @@ class RepositoryViewerController
* The repository viewer. * The repository viewer.
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService & RepositoryCommitFileService & AccountService & ActivityService & IssuesService & self: RepositoryService
WebHookService & CommitsService & ReadableUsersAuthenticator & ReferrerAuthenticator & WritableUsersAuthenticator & with RepositoryCommitFileService
PullRequestService & CommitStatusService & WebHookPullRequestService & WebHookPullRequestReviewCommentService & with AccountService
ProtectedBranchService => with ActivityService
with IssuesService
with WebHookService
with CommitsService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with PullRequestService
with CommitStatusService
with WebHookPullRequestService
with WebHookPullRequestReviewCommentService
with ProtectedBranchService =>
ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat) ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
private case class UploadForm( case class UploadForm(
branch: String, branch: String,
path: String, path: String,
uploadFiles: String, uploadFiles: String,
@@ -82,7 +93,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
newBranch: Boolean newBranch: Boolean
) )
private case class EditorForm( case class EditorForm(
branch: String, branch: String,
path: String, path: String,
content: String, content: String,
@@ -95,7 +106,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
newBranch: Boolean newBranch: Boolean
) )
private case class DeleteForm( case class DeleteForm(
branch: String, branch: String,
path: String, path: String,
message: Option[String], message: Option[String],
@@ -104,7 +115,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
newBranch: Boolean newBranch: Boolean
) )
private case class CommentForm( case class CommentForm(
fileName: Option[String], fileName: Option[String],
oldLineNumber: Option[Int], oldLineNumber: Option[Int],
newLineNumber: Option[Int], newLineNumber: Option[Int],
@@ -113,13 +124,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
diff: Option[String] diff: Option[String]
) )
private case class TagForm( case class TagForm(
commitId: String, commitId: String,
tagName: String, tagName: String,
message: Option[String] message: Option[String]
) )
private val uploadForm = mapping( val uploadForm = mapping(
"branch" -> trim(label("Branch", text(required))), "branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())), "path" -> trim(label("Path", text())),
"uploadFiles" -> trim(label("Upload files", text(required))), "uploadFiles" -> trim(label("Upload files", text(required))),
@@ -128,7 +139,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"newBranch" -> trim(label("New Branch", boolean())) "newBranch" -> trim(label("New Branch", boolean()))
)(UploadForm.apply) )(UploadForm.apply)
private val editorForm = mapping( val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))), "branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())), "path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))), "content" -> trim(label("Content", text(required))),
@@ -141,7 +152,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"newBranch" -> trim(label("New Branch", boolean())) "newBranch" -> trim(label("New Branch", boolean()))
)(EditorForm.apply) )(EditorForm.apply)
private val deleteForm = mapping( val deleteForm = mapping(
"branch" -> trim(label("Branch", text(required))), "branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())), "path" -> trim(label("Path", text())),
"message" -> trim(label("Message", optional(text()))), "message" -> trim(label("Message", optional(text()))),
@@ -150,7 +161,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"newBranch" -> trim(label("New Branch", boolean())) "newBranch" -> trim(label("New Branch", boolean()))
)(DeleteForm.apply) )(DeleteForm.apply)
private val commentForm = mapping( val commentForm = mapping(
"fileName" -> trim(label("Filename", optional(text()))), "fileName" -> trim(label("Filename", optional(text()))),
"oldLineNumber" -> trim(label("Old line number", optional(number()))), "oldLineNumber" -> trim(label("Old line number", optional(number()))),
"newLineNumber" -> trim(label("New line number", optional(number()))), "newLineNumber" -> trim(label("New line number", optional(number()))),
@@ -159,7 +170,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"diff" -> optional(text()) "diff" -> optional(text())
)(CommentForm.apply) )(CommentForm.apply)
private val tagForm = mapping( val tagForm = mapping(
"commitId" -> trim(label("Commit id", text(required))), "commitId" -> trim(label("Commit id", text(required))),
"tagName" -> trim(label("Tag name", text(required))), "tagName" -> trim(label("Tag name", text(required))),
"message" -> trim(label("Message", optional(text()))) "message" -> trim(label("Message", optional(text())))
@@ -246,7 +257,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (branchName, path) = repository.splitPath(multiParams("splat").head) val (branchName, path) = repository.splitPath(multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match { JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
html.commits( html.commits(
@@ -261,7 +273,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getCommitStatusWithSummary(repository.owner, repository.name, commit.id) getCommitStatusWithSummary(repository.owner, repository.name, commit.id)
) )
} }
.splitWith { case ((commit1, _, _), (commit2, _, _)) => .splitWith {
case ((commit1, _, _), (commit2, _, _)) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, },
page, page,
@@ -274,7 +287,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/new/*")(writableUsersOnly { repository => get("/:owner/:repository/new/*")(writableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val (branch, path) = repository.splitPath(multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch)
.needStatusCheck(loginAccount.userName) .needStatusCheck(loginAccount.userName)
@@ -285,7 +299,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.editor( html.editor(
branch = branch, branch = branch,
repository = repository, repository = repository,
pathList = if (path.isEmpty) Nil else path.split("/").toList, pathList = if (path.length == 0) Nil else path.split("/").toList,
fileName = None, fileName = None,
content = JGitUtil.ContentInfo("text", None, None, Some("UTF-8")), content = JGitUtil.ContentInfo("text", None, None, Some("UTF-8")),
protectedBranch = protectedBranch, protectedBranch = protectedBranch,
@@ -296,7 +310,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/upload/*")(writableUsersOnly { repository => get("/:owner/:repository/upload/*")(writableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val (branch, path) = repository.splitPath(multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch)
.needStatusCheck(loginAccount.userName) .needStatusCheck(loginAccount.userName)
@@ -305,7 +320,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.upload( html.upload(
branch, branch,
repository, repository,
if (path.isEmpty) Nil else path.split("/").toList, if (path.length == 0) Nil else path.split("/").toList,
protectedBranch, protectedBranch,
revCommit.name revCommit.name
) )
@@ -325,7 +340,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
message = form.message.getOrElse("Add files via upload"), message = form.message.getOrElse("Add files via upload"),
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
) { case (git, headTip, builder, inserter) => ) {
case (git, headTip, builder, inserter) =>
JGitUtil.processTree(git, headTip) { (path, tree) => JGitUtil.processTree(git, headTip) { (path, tree) =>
if (!newFiles.exists(_.name.contains(path))) { if (!newFiles.exists(_.name.contains(path))) {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
@@ -343,7 +359,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
} }
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val files = form.uploadFiles val files = form.uploadFiles
.split("\n") .split("\n")
.map { line => .map { line =>
@@ -353,7 +370,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
.toSeq .toSeq
val newFiles = files.map { file => val newFiles = files.map { file =>
file.copy(name = if (form.path.isEmpty) file.name else s"${form.path}/${file.name}") file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
} }
if (form.newBranch) { if (form.newBranch) {
@@ -370,13 +387,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
form.message, form.message,
loginAccount loginAccount
) )
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error)) case Left(error) => Forbidden(gitbucket.core.html.error(error))
} }
} else { } else {
_commit(form.branch, newFiles, loginAccount) match { _commit(form.branch, newFiles, loginAccount) match {
case Right(_) => case Right(_) =>
if (form.path.isEmpty) { if (form.path.length == 0) {
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}") redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
} else { } else {
redirect( redirect(
@@ -390,16 +407,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/edit/*")(writableUsersOnly { repository => get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val (branch, path) = repository.splitPath(multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch)
.needStatusCheck(loginAccount.userName) .needStatusCheck(loginAccount.userName)
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit) getPathObjectId(git, path, revCommit)
.map { objectId => .map {
objectId =>
val paths = path.split("/") val paths = path.split("/")
val info = EditorConfigUtil.getEditorConfigInfo(git, branch, path) val info = EditorConfigUtil.getEditorConfigInfo(git, branch, path)
@@ -422,7 +442,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/remove/*")(writableUsersOnly { repository => get("/:owner/:repository/remove/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId => getPathObjectId(git, path, revCommit).map { objectId =>
@@ -456,7 +477,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
).map(_._1) ).map(_._1)
} }
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (form.newBranch) { if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount) val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, loginAccount) match { _commit(newBranchName, loginAccount) match {
@@ -471,13 +493,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
form.message, form.message,
loginAccount loginAccount
) )
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error)) case Left(error) => Forbidden(gitbucket.core.html.error(error))
} }
} else { } else {
_commit(form.branch, loginAccount) match { _commit(form.branch, loginAccount) match {
case Right(_) => case Right(_) =>
if (form.path.isEmpty) { if (form.path.length == 0) {
redirect( redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}" s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
) )
@@ -513,7 +535,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
).map(_._1) ).map(_._1)
} }
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (form.newBranch) { if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount) val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, loginAccount) match { _commit(newBranchName, loginAccount) match {
@@ -528,13 +551,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
form.message, form.message,
loginAccount loginAccount
) )
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error)) case Left(error) => Forbidden(gitbucket.core.html.error(error))
} }
} else { } else {
_commit(form.branch, loginAccount) match { _commit(form.branch, loginAccount) match {
case Right(_) => case Right(_) =>
if (form.path.isEmpty) { if (form.path.length == 0) {
redirect( redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}" s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
) )
@@ -566,7 +589,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
).map(_._1) ).map(_._1)
} }
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (form.newBranch) { if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount) val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, loginAccount) match { _commit(newBranchName, loginAccount) match {
@@ -581,7 +605,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
form.message, form.message,
loginAccount loginAccount
) )
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error)) case Left(error) => Forbidden(gitbucket.core.html.error(error))
} }
} else { } else {
@@ -629,7 +653,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
sender, sender,
repository, repository,
owner, owner,
ref = s"refs/heads/$newBranchName", ref = newBranchName,
refType = "branch" refType = "branch"
) )
} }
@@ -686,13 +710,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/** /**
* Displays the file content of the specified branch or commit. * Displays the file content of the specified branch or commit.
*/ */
private val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository => val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val (id, path) = repository.splitPath(multiParams("splat").head) val (id, path) = repository.splitPath(multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean val raw = params.get("raw").getOrElse("false").toBoolean
val highlighterTheme = getSyntaxHighlighterTheme() val highlighterTheme = getSyntaxHighlighterTheme()
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).map { objectId => getPathObjectId(git, path, revCommit).map {
objectId =>
if (raw) { if (raw) {
// Download (This route is left for backward compatibility) // Download (This route is left for backward compatibility)
responseRawFile(git, objectId, path, repository) responseRawFile(git, objectId, path, repository)
@@ -740,7 +766,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository => ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository =>
val (id, path) = repository.splitPath(multiParams("splat").head) val (id, path) = repository.splitPath(multiParams("splat").head)
contentType = formats("json") contentType = formats("json")
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
Serialization.write( Serialization.write(
Map( Map(
@@ -748,7 +775,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"id" -> id, "id" -> id,
"path" -> path, "path" -> path,
"last" -> last, "last" -> last,
"blame" -> JGitUtil.getBlame(git, id, path).map { blame => "blame" -> JGitUtil.getBlame(git, id, path).map {
blame =>
Map( Map(
"id" -> blame.id, "id" -> blame.id,
"author" -> view.helpers.user(blame.authorName, blame.authorEmailAddress).toString, "author" -> view.helpers.user(blame.authorName, blame.authorEmailAddress).toString,
@@ -773,17 +801,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val id = params("id") val id = params("id")
try { try {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
val diffs = JGitUtil.getDiffs( val diffs = JGitUtil.getDiffs(git, None, id, true, false)
git = git,
from = None,
to = id,
fetchContent = true,
makePatch = false,
maxFiles = context.settings.repositoryViewer.maxDiffFiles,
maxLines = context.settings.repositoryViewer.maxDiffLines
)
val oldCommitId = JGitUtil.getParentCommitId(git, id) val oldCommitId = JGitUtil.getParentCommitId(git, id)
html.commit( html.commit(
@@ -792,7 +813,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
getCommitStatusWithSummary(repository.owner, repository.name, revCommit.getName), getCommitStatusWithSummary(repository.owner, repository.name, revCommit.getName),
getCommitComments(repository.owner, repository.name, id, includePullRequest = true), getCommitComments(repository.owner, repository.name, id, true),
repository, repository,
diffs, diffs,
oldCommitId, oldCommitId,
@@ -802,7 +823,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
) )
} }
} catch { } catch {
case _: MissingObjectException => NotFound() case e: MissingObjectException => NotFound()
} }
}) })
@@ -814,7 +835,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
diff diff
} }
} catch { } catch {
case _: MissingObjectException => NotFound() case e: MissingObjectException => NotFound()
} }
}) })
@@ -827,7 +848,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
diff diff
} }
} catch { } catch {
case _: MissingObjectException => NotFound() case e: MissingObjectException => NotFound()
} }
}) })
@@ -846,7 +867,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
form.issueId form.issueId
) )
redirect(s"/${repository.owner}/${repository.name}/commit/$id") redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
} }
}) })
@@ -869,7 +890,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val id = params("id") val id = params("id")
val commentId = createCommitComment( val commentId = createCommitComment(
repository, repository,
@@ -890,8 +912,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
getCommitComment(repository.owner, repository.name, params("id")) map { x => loginAccount =>
getCommitComment(repository.owner, repository.name, params("id")) map {
x =>
if (isEditable(x.userName, x.repositoryName, x.commentedUserName, loginAccount)) { if (isEditable(x.userName, x.repositoryName, x.commentedUserName, loginAccount)) {
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => html.editcomment(x.content, x.commentId, repository) case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
@@ -919,7 +943,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
getCommitComment(repository.owner, repository.name, params("id")).map { comment => getCommitComment(repository.owner, repository.name, params("id")).map { comment =>
if (isEditable(repository.owner, repository.name, comment.commentedUserName, loginAccount)) { if (isEditable(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
updateCommitComment(comment.commentId, form.content) updateCommitComment(comment.commentId, form.content)
@@ -944,7 +969,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/ */
get("/:owner/:repository/branches")(referrersOnly { repository => get("/:owner/:repository/branches")(referrersOnly { repository =>
val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet
val branches = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => val branches = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
JGitUtil JGitUtil
.getBranches( .getBranches(
git = git, git = git,
@@ -952,7 +978,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
origin = repository.repository.originUserName.isEmpty origin = repository.repository.originUserName.isEmpty
) )
.sortBy(branch => (branch.mergeInfo.isEmpty, branch.commitTime)) .sortBy(branch => (branch.mergeInfo.isEmpty, branch.commitTime))
.map(branch => .map(
branch =>
( (
branch, branch,
getPullRequestByRequestCommit( getPullRequestByRequestCommit(
@@ -1001,12 +1028,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/branches")(writableUsersOnly { repository => post("/:owner/:repository/branches")(writableUsersOnly { repository =>
val newBranchName = params.getOrElse("new", halt(400)) val newBranchName = params.getOrElse("new", halt(400))
val fromBranchName = params.getOrElse("from", halt(400)) val fromBranchName = params.getOrElse("from", halt(400))
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
JGitUtil.createBranch(git, fromBranchName, newBranchName) match { JGitUtil.createBranch(git, fromBranchName, newBranchName) match {
case Right(message) => case Right(message) =>
flash.update("info", message) flash.update("info", message)
val settings = loadSystemSettings() val settings = loadSystemSettings()
val newCommitId = git.getRepository.resolve(s"refs/heads/$newBranchName") val newCommitId = git.getRepository.resolve(s"refs/heads/${newBranchName}")
val oldCommitId = ObjectId.fromString("0" * 40) val oldCommitId = ObjectId.fromString("0" * 40)
// call push webhook // call push webhook
callWebHookOf(repository.owner, repository.name, WebHook.Push, settings) { callWebHookOf(repository.owner, repository.name, WebHook.Push, settings) {
@@ -1017,7 +1045,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
WebHookPushPayload( WebHookPushPayload(
git, git,
pusherAccount, pusherAccount,
s"refs/heads/$newBranchName", newBranchName,
repository, repository,
List(), List(),
ownerAccount, ownerAccount,
@@ -1036,7 +1064,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
sender, sender,
repository, repository,
owner, owner,
ref = s"refs/heads/$newBranchName", ref = newBranchName,
refType = "branch" refType = "branch"
) )
} }
@@ -1046,7 +1074,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
) )
case Left(message) => case Left(message) =>
flash.update("error", message) flash.update("error", message)
redirect(s"/${repository.owner}/${repository.name}/tree/$fromBranchName") redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
} }
} }
}) })
@@ -1055,7 +1083,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Deletes branch. * Deletes branch.
*/ */
get("/:owner/:repository/delete/*")(writableUsersOnly { repository => get("/:owner/:repository/delete/*")(writableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
val branchName = multiParams("splat").head val branchName = multiParams("splat").head
if (repository.repository.defaultBranch != branchName) { if (repository.repository.defaultBranch != branchName) {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
@@ -1132,9 +1161,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
// case class UploadFiles(branch: String, path: String, fileIds: Map[String, String], message: String) { case class UploadFiles(branch: String, path: String, fileIds: Map[String, String], message: String) {
// lazy val isValid: Boolean = fileIds.nonEmpty lazy val isValid: Boolean = fileIds.nonEmpty
// } }
/** /**
* Provides HTML of the file list. * Provides HTML of the file list.
@@ -1150,7 +1179,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.guide(repository, hasDeveloperRole(repository.owner, repository.name, context.loginAccount)) html.guide(repository, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
} else { } else {
// get specified commit // get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => JGitUtil.getDefaultBranch(git, repository, revstr).map {
case (objectId, revision) =>
val revCommit = JGitUtil.getRevCommitFromId(git, objectId) val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
val lastModifiedCommit = val lastModifiedCommit =
if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
@@ -1174,11 +1204,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val path = (file.name :: parentPath.reverse).reverse val path = (file.name :: parentPath.reverse).reverse
path -> StringUtil.convertFromByteArray( path -> StringUtil.convertFromByteArray(
JGitUtil JGitUtil
.getContentFromId( .getContentFromId(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true)
Git.open(getRepositoryDir(repository.owner, repository.name)),
file.id,
fetchLargeFile = true
)
.get .get
) )
} }
@@ -1212,14 +1238,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
path: String path: String
) = { ) = {
def archive[A <: ArchiveEntry](revision: String, archiveFormat: String, archive: ArchiveOutputStream[A])( def archive(revision: String, archiveFormat: String, archive: ArchiveOutputStream)(
entryCreator: (String, Long, java.util.Date, Int) => A entryCreator: (String, Long, java.util.Date, Int) => ArchiveEntry
): Unit = { ): Unit = {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val oid = git.getRepository.resolve(revision) val oid = git.getRepository.resolve(revision)
val commit = JGitUtil.getRevCommitFromId(git, oid) val commit = JGitUtil.getRevCommitFromId(git, oid)
val date = commit.getCommitterIdent.getWhen val date = commit.getCommitterIdent.getWhen
val sha1 = oid.getName val sha1 = oid.getName()
val repositorySuffix = (if (sha1.startsWith(revision)) sha1 else revision).replace('/', '-') val repositorySuffix = (if (sha1.startsWith(revision)) sha1 else revision).replace('/', '-')
val pathSuffix = if (path.isEmpty) "" else s"-${path.replace('/', '-')}" val pathSuffix = if (path.isEmpty) "" else s"-${path.replace('/', '-')}"
val baseName = repository.name + "-" + repositorySuffix + pathSuffix val baseName = repository.name + "-" + repositorySuffix + pathSuffix
@@ -1227,7 +1253,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
Using.resource(new TreeWalk(git.getRepository)) { treeWalk => Using.resource(new TreeWalk(git.getRepository)) { treeWalk =>
treeWalk.addTree(commit.getTree) treeWalk.addTree(commit.getTree)
treeWalk.setRecursive(true) treeWalk.setRecursive(true)
if (path.nonEmpty) { if (!path.isEmpty) {
treeWalk.setFilter(PathFilter.create(path)) treeWalk.setFilter(PathFilter.create(path))
} }
if (treeWalk != null) { if (treeWalk != null) {
@@ -1255,7 +1281,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
) )
} }
val entry: A = entryCreator(entryPath, size, date, mode) val entry: ArchiveEntry = entryCreator(entryPath, size, date, mode)
archive.putArchiveEntry(entry) archive.putArchiveEntry(entry)
Using.resource(new FileInputStream(tempFile)) { in => Using.resource(new FileInputStream(tempFile)) { in =>
IOUtils.copy(in, archive) IOUtils.copy(in, archive)
@@ -1271,7 +1297,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
val suffix = val suffix =
path.split("/").lastOption.collect { case x if x.nonEmpty => "-" + x.replace('/', '_') }.getOrElse("") path.split("/").lastOption.collect { case x if x.length > 0 => "-" + x.replace('/', '_') }.getOrElse("")
val zipRe = """(.+)\.zip$""".r val zipRe = """(.+)\.zip$""".r
val tarRe = """(.+)\.tar\.(gz|bz2|xz)$""".r val tarRe = """(.+)\.tar\.(gz|bz2|xz)$""".r
@@ -1279,7 +1305,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case zipRe(revision) => case zipRe(revision) =>
response.setHeader( response.setHeader(
"Content-Disposition", "Content-Disposition",
s"attachment; filename=${repository.name}-$revision$suffix.zip" s"attachment; filename=${repository.name}-${revision}${suffix}.zip"
) )
contentType = "application/octet-stream" contentType = "application/octet-stream"
response.setBufferSize(1024 * 1024) response.setBufferSize(1024 * 1024)
@@ -1296,7 +1322,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case tarRe(revision, compressor) => case tarRe(revision, compressor) =>
response.setHeader( response.setHeader(
"Content-Disposition", "Content-Disposition",
s"attachment; filename=${repository.name}-$revision$suffix.tar.$compressor" s"attachment; filename=${repository.name}-${revision}${suffix}.tar.${compressor}"
) )
contentType = "application/octet-stream" contentType = "application/octet-stream"
response.setBufferSize(1024 * 1024) response.setBufferSize(1024 * 1024)
@@ -1334,9 +1360,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val repository = params("repository") val repository = params("repository")
val branch = params("branch") val branch = params("branch")
LockUtil.lock(s"$owner/$repository") { LockUtil.lock(s"${owner}/${repository}") {
Using.resource(Git.open(getRepositoryDir(owner, repository))) { git => Using.resource(Git.open(getRepositoryDir(owner, repository))) { git =>
val headName = s"refs/heads/$branch" val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName) val headTip = git.getRepository.resolve(headName)
if (headTip.getName != value) { if (headTip.getName != value) {
Some("Someone pushed new commits before you. Please reload this page and re-apply your changes.") Some("Someone pushed new commits before you. Please reload this page and re-apply your changes.")

View File

@@ -3,17 +3,17 @@ package gitbucket.core.controller
import java.io.FileInputStream import java.io.FileInputStream
import gitbucket.core.admin.html import gitbucket.core.admin.html
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.* import gitbucket.core.service.SystemSettingsService._
import gitbucket.core.service.{AccountService, RepositoryService} import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.ssh.SshServer import gitbucket.core.ssh.SshServer
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil.* import gitbucket.core.util.StringUtil._
import gitbucket.core.util.{AdminAuthenticator, Mailer} import gitbucket.core.util.{AdminAuthenticator, Mailer}
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.mail.EmailException import org.apache.commons.mail.EmailException
import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization
import org.scalatra.* import org.scalatra._
import org.scalatra.forms.* import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
@@ -29,7 +29,7 @@ case class Table(name: String, columns: Seq[Column])
case class Column(name: String, primaryKey: Boolean) case class Column(name: String, primaryKey: Boolean)
trait SystemSettingsControllerBase extends AccountManagementControllerBase { trait SystemSettingsControllerBase extends AccountManagementControllerBase {
self: AccountService & RepositoryService & AdminAuthenticator => self: AccountService with RepositoryService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
@@ -55,13 +55,15 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"bindAddress" -> mapping( "bindAddress" -> mapping(
"host" -> trim(label("Bind SSH host", optional(text()))), "host" -> trim(label("Bind SSH host", optional(text()))),
"port" -> trim(label("Bind SSH port", optional(number()))), "port" -> trim(label("Bind SSH port", optional(number()))),
)((hostOption, portOption) => )(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser)) hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
), ),
"publicAddress" -> mapping( "publicAddress" -> mapping(
"host" -> trim(label("Public SSH host", optional(text()))), "host" -> trim(label("Public SSH host", optional(text()))),
"port" -> trim(label("Public SSH port", optional(number()))), "port" -> trim(label("Public SSH port", optional(number()))),
)((hostOption, portOption) => )(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser)) hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
), ),
)(Ssh.apply), )(Ssh.apply),
@@ -121,11 +123,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"largeTimeout" -> trim(label("Timeout for large file", long(required))) "largeTimeout" -> trim(label("Timeout for large file", long(required)))
)(Upload.apply), )(Upload.apply),
"repositoryViewer" -> mapping( "repositoryViewer" -> mapping(
"maxFiles" -> trim(label("Max files", number(required))), "maxFiles" -> trim(label("Max files", number(required)))
"maxDiffFiles" -> trim(label("Max diff files", number(required))), )(RepositoryViewerSettings.apply)
"maxDiffLines" -> trim(label("Max diff lines", number(required)))
)(RepositoryViewerSettings.apply),
"defaultBranch" -> trim(label("Default branch", text(required)))
)(SystemSettings.apply).verifying { settings => )(SystemSettings.apply).verifying { settings =>
Vector( Vector(
if (settings.ssh.enabled && settings.baseUrl.isEmpty) { if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
@@ -151,11 +150,11 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"testAddress" -> trim(label("", text(required))) "testAddress" -> trim(label("", text(required)))
)(SendMailForm.apply) )(SendMailForm.apply)
private case class SendMailForm(smtp: Smtp, testAddress: String) case class SendMailForm(smtp: Smtp, testAddress: String)
// case class DataExportForm(tableNames: List[String]) case class DataExportForm(tableNames: List[String])
private case class NewUserForm( case class NewUserForm(
userName: String, userName: String,
password: String, password: String,
fullName: String, fullName: String,
@@ -167,7 +166,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
fileId: Option[String] fileId: Option[String]
) )
private case class EditUserForm( case class EditUserForm(
userName: String, userName: String,
password: Option[String], password: Option[String],
fullName: String, fullName: String,
@@ -181,7 +180,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
isRemoved: Boolean isRemoved: Boolean
) )
private case class NewGroupForm( case class NewGroupForm(
groupName: String, groupName: String,
description: Option[String], description: Option[String],
url: Option[String], url: Option[String],
@@ -189,7 +188,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
members: String members: String
) )
private case class EditGroupForm( case class EditGroupForm(
groupName: String, groupName: String,
description: Option[String], description: Option[String],
url: Option[String], url: Option[String],
@@ -199,7 +198,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
isRemoved: Boolean isRemoved: Boolean
) )
private val newUserForm = mapping( val newUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "userName" -> trim(label("Username", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password", text(required, maxlength(40)))), "password" -> trim(label("Password", text(required, maxlength(40)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))), "fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
@@ -213,7 +212,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"fileId" -> trim(label("File ID", optional(text()))) "fileId" -> trim(label("File ID", optional(text())))
)(NewUserForm.apply) )(NewUserForm.apply)
private val editUserForm = mapping( val editUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier))), "userName" -> trim(label("Username", text(required, maxlength(100), identifier))),
"password" -> trim(label("Password", optional(text(maxlength(40))))), "password" -> trim(label("Password", optional(text(maxlength(40))))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))), "fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
@@ -229,7 +228,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"removed" -> trim(label("Disable", boolean(disableByNotYourself("userName")))) "removed" -> trim(label("Disable", boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply) )(EditUserForm.apply)
private val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"description" -> trim(label("Group description", optional(text()))), "description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))), "url" -> trim(label("URL", optional(text(maxlength(200))))),
@@ -237,7 +236,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"members" -> trim(label("Members", text(required, members))) "members" -> trim(label("Members", text(required, members)))
)(NewGroupForm.apply) )(NewGroupForm.apply)
private val editGroupForm = mapping( val editGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier))),
"description" -> trim(label("Group description", optional(text()))), "description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))), "url" -> trim(label("URL", optional(text(maxlength(200))))),
@@ -251,7 +250,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val conn = request2Session(request).conn val conn = request2Session(request).conn
val meta = conn.getMetaData val meta = conn.getMetaData
val tables = ListBuffer[Table]() val tables = ListBuffer[Table]()
Using.resource(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))) { rs => Using.resource(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))) {
rs =>
while (rs.next()) { while (rs.next()) {
val tableName = rs.getString("TABLE_NAME") val tableName = rs.getString("TABLE_NAME")
@@ -284,9 +284,11 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
if (trimmedQuery.nonEmpty) { if (trimmedQuery.nonEmpty) {
try { try {
val conn = request2Session(request).conn val conn = request2Session(request).conn
Using.resource(conn.prepareStatement(query)) { stmt => Using.resource(conn.prepareStatement(query)) {
stmt =>
if (trimmedQuery.toUpperCase.startsWith("SELECT")) { if (trimmedQuery.toUpperCase.startsWith("SELECT")) {
Using.resource(stmt.executeQuery()) { rs => Using.resource(stmt.executeQuery()) {
rs =>
val meta = rs.getMetaData val meta = rs.getMetaData
val columns = for (i <- 1 to meta.getColumnCount) yield { val columns = for (i <- 1 to meta.getColumnCount) yield {
meta.getColumnName(i) meta.getColumnName(i)
@@ -320,9 +322,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form) saveSystemSettings(form)
if ( if (form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress) {
form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress
) {
SshServer.stop() SshServer.stop()
for { for {
bindAddress <- form.ssh.bindAddress bindAddress <- form.ssh.bindAddress
@@ -363,7 +363,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
}) })
post("/admin/plugins/_reload")(adminOnly { post("/admin/plugins/_reload")(adminOnly {
PluginRegistry.reload(request.getServletContext, loadSystemSettings(), request2Session(request).conn) PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn)
flash.update("info", "All plugins were reloaded.") flash.update("info", "All plugins were reloaded.")
redirect("/admin/plugins") redirect("/admin/plugins")
}) })
@@ -385,7 +385,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val includeGroups = params.get("includeGroups").exists(_.toBoolean) val includeGroups = params.get("includeGroups").exists(_.toBoolean)
val users = getAllUsers(includeRemoved, includeGroups) val users = getAllUsers(includeRemoved, includeGroups)
val members = users.collect { val members = users.collect {
case account if account.isGroupAccount => case account if (account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName) account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap }.toMap
@@ -406,7 +406,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
form.description, form.description,
form.url form.url
) )
updateImage(form.userName, form.fileId, clearImage = false) updateImage(form.userName, form.fileId, false)
updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != "")) updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != ""))
redirect("/admin/users") redirect("/admin/users")
}) })
@@ -414,12 +414,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
get("/admin/users/:userName/_edituser")(adminOnly { get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName") val userName = params("userName")
val extraMails = getAccountExtraMailAddresses(userName) val extraMails = getAccountExtraMailAddresses(userName)
html.user(getAccountByUserName(userName, includeRemoved = true), extraMails, flash.get("error")) html.user(getAccountByUserName(userName, true), extraMails, flash.get("error"))
}) })
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName, includeRemoved = true).map { account => getAccountByUserName(userName, true).map {
account =>
if (account.isAdmin && (form.isRemoved || !form.isAdmin) && isLastAdministrator(account)) { if (account.isAdmin && (form.isRemoved || !form.isAdmin) && isLastAdministrator(account)) {
flash.update("error", "Account can't be turned off because this is last one administrator.") flash.update("error", "Account can't be turned off because this is last one administrator.")
redirect(s"/admin/users/${userName}/_edituser") redirect(s"/admin/users/${userName}/_edituser")
@@ -476,13 +477,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
} }
.toList .toList
) )
updateImage(form.groupName, form.fileId, clearImage = false) updateImage(form.groupName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
get("/admin/users/:groupName/_editgroup")(adminOnly { get("/admin/users/:groupName/_editgroup")(adminOnly {
val groupName = params("groupName") val groupName = params("groupName")
html.usergroup(getAccountByUserName(groupName, includeRemoved = true), getGroupMembers(groupName)) html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}) })
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
@@ -496,7 +497,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
} }
.toList .toList
getAccountByUserName(groupName, includeRemoved = true).map { account => getAccountByUserName(groupName, true).map {
account =>
updateGroup(groupName, form.description, form.url, form.isRemoved) updateGroup(groupName, form.description, form.url, form.isRemoved)
if (form.isRemoved) { if (form.isRemoved) {
@@ -528,13 +530,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
}) })
get("/admin/data")(adminOnly { get("/admin/data")(adminOnly {
import gitbucket.core.util.JDBCUtil.* import gitbucket.core.util.JDBCUtil._
val session = request2Session(request) val session = request2Session(request)
html.data(session.conn.allTableNames()) html.data(session.conn.allTableNames())
}) })
post("/admin/export")(adminOnly { post("/admin/export")(adminOnly {
import gitbucket.core.util.JDBCUtil.* import gitbucket.core.util.JDBCUtil._
val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq) val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
contentType = "application/octet-stream" contentType = "application/octet-stream"
@@ -549,7 +551,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
}) })
private def multiLineText(constraints: Constraint*): SingleValueType[Seq[String]] = private def multiLineText(constraints: Constraint*): SingleValueType[Seq[String]] =
new SingleValueType[Seq[String]](constraints*) { new SingleValueType[Seq[String]](constraints: _*) {
def convert(value: String, messages: Messages): Seq[String] = { def convert(value: String, messages: Messages): Seq[String] = {
if (value == null) { if (value == null) {
Nil Nil
@@ -562,11 +564,9 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
private def members: Constraint = private def members: Constraint =
new Constraint() { new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
if ( if (value.split(",").exists {
value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean } _.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
} }) None
) None
else Some("Must select one manager at least.") else Some("Must select one manager at least.")
} }
} }
@@ -577,7 +577,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
for { for {
userName <- params.get(paramName) userName <- params.get(paramName)
loginAccount <- context.loginAccount loginAccount <- context.loginAccount
if userName == loginAccount.userName && params.get("removed").contains("true") if userName == loginAccount.userName && params.get("removed") == Some("true")
} yield "You can't disable your account yourself" } yield "You can't disable your account yourself"
} }
} }

View File

@@ -1,16 +1,16 @@
package gitbucket.core.controller package gitbucket.core.controller
import org.json4s.{JField, JObject, JString} import org.json4s.{JField, JObject, JString}
import org.scalatra.* import org.scalatra._
import org.scalatra.json.* import org.scalatra.json._
import org.scalatra.forms.* import org.scalatra.forms._
import org.scalatra.i18n.I18nSupport import org.scalatra.i18n.I18nSupport
import org.scalatra.servlet.ServletBase import org.scalatra.servlet.ServletBase
/** /**
* Extends scalatra-forms to support the client-side validation and Ajax requests as well. * Extends scalatra-forms to support the client-side validation and Ajax requests as well.
*/ */
trait ValidationSupport extends FormSupport { self: ServletBase & JacksonJsonSupport & I18nSupport => trait ValidationSupport extends FormSupport { self: ServletBase with JacksonJsonSupport with I18nSupport =>
def get[T](path: String, form: ValueType[T])(action: T => Any): Route = { def get[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form) registerValidate(path, form)
@@ -84,7 +84,8 @@ trait ValidationSupport extends FormSupport { self: ServletBase & JacksonJsonSup
* Converts errors to JSON. * Converts errors to JSON.
*/ */
private def toJson(errors: Seq[(String, String)]): JObject = private def toJson(errors: Seq[(String, String)]): JObject =
JObject(errors.map { case (key, value) => JObject(errors.map {
case (key, value) =>
JField(key, JString(value)) JField(key, JString(value))
}.toList) }.toList)

View File

@@ -5,13 +5,13 @@ import gitbucket.core.model.activity.{CreateWikiPageInfo, DeleteWikiInfo, EditWi
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.WebHookService.WebHookGollumPayload import gitbucket.core.service.WebHookService.WebHookGollumPayload
import gitbucket.core.wiki.html import gitbucket.core.wiki.html
import gitbucket.core.service.* import gitbucket.core.service._
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.util.StringUtil.* import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars.* import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory._
import org.scalatra.forms.* import org.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
@@ -29,10 +29,15 @@ class WikiController
with RequestCache with RequestCache
trait WikiControllerBase extends ControllerBase { trait WikiControllerBase extends ControllerBase {
self: WikiService & RepositoryService & AccountService & ActivityService & WebHookService & self: WikiService
ReadableUsersAuthenticator & ReferrerAuthenticator => with RepositoryService
with AccountService
with ActivityService
with WebHookService
with ReadableUsersAuthenticator
with ReferrerAuthenticator =>
private case class WikiPageEditForm( case class WikiPageEditForm(
pageName: String, pageName: String,
content: String, content: String,
message: Option[String], message: Option[String],
@@ -40,16 +45,16 @@ trait WikiControllerBase extends ControllerBase {
id: String id: String
) )
private val newForm = mapping( val newForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pageName, unique))), "pageName" -> trim(label("Page name", text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content", text(required, conflictForNew))), "content" -> trim(label("Content", text(required, conflictForNew))),
"message" -> trim(label("Message", optional(text()))), "message" -> trim(label("Message", optional(text()))),
"currentPageName" -> trim(label("Current page name", text())), "currentPageName" -> trim(label("Current page name", text())),
"id" -> trim(label("Latest commit id", text())) "id" -> trim(label("Latest commit id", text()))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
private val editForm = mapping( val editForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pageName))), "pageName" -> trim(label("Page name", text(required, maxlength(40), pagename))),
"content" -> trim(label("Content", text(required, conflictForEdit))), "content" -> trim(label("Content", text(required, conflictForEdit))),
"message" -> trim(label("Message", optional(text()))), "message" -> trim(label("Message", optional(text()))),
"currentPageName" -> trim(label("Current page name", text(required))), "currentPageName" -> trim(label("Current page name", text(required))),
@@ -57,56 +62,46 @@ trait WikiControllerBase extends ControllerBase {
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
val branch = getWikiBranch(repository.owner, repository.name) getWikiPage(repository.owner, repository.name, "Home").map { page =>
getWikiPage(repository.owner, repository.name, "Home", branch).map { page =>
html.page( html.page(
"Home", "Home",
page, page,
getWikiPageList(repository.owner, repository.name, branch), getWikiPageList(repository.owner, repository.name),
repository, repository,
isEditable(repository), isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar", branch), getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer", branch) getWikiPage(repository.owner, repository.name, "_Footer")
) )
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
}) })
get("/:owner/:repository/wiki/:page")(referrersOnly { repository => get("/:owner/:repository/wiki/:page")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val branch = getWikiBranch(repository.owner, repository.name)
getWikiPage(repository.owner, repository.name, pageName, branch).map { page => getWikiPage(repository.owner, repository.name, pageName).map { page =>
html.page( html.page(
pageName, pageName,
page, page,
getWikiPageList(repository.owner, repository.name, branch), getWikiPageList(repository.owner, repository.name),
repository, repository,
isEditable(repository), isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar", branch), getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer", branch) getWikiPage(repository.owner, repository.name, "_Footer")
) )
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
}) })
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val branch = getWikiBranch(repository.owner, repository.name)
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
JGitUtil.getCommitLog(git, branch, path = pageName + ".md") match { JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository, isEditable(repository)) case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository, isEditable(repository))
case Left(_) => NotFound() case Left(_) => NotFound()
} }
} }
}) })
private def getWikiBranch(owner: String, repository: String): String = {
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
git.getRepository.getBranch
}
}
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
@@ -116,17 +111,7 @@ trait WikiControllerBase extends ControllerBase {
Some(pageName), Some(pageName),
from, from,
to, to,
JGitUtil JGitUtil.getDiffs(git, Some(from), to, true, false).filter(_.newPath == pageName + ".md"),
.getDiffs(
git = git,
from = Some(from),
to = to,
fetchContent = true,
makePatch = false,
maxFiles = context.settings.repositoryViewer.maxDiffFiles,
maxLines = context.settings.repositoryViewer.maxDiffLines
)
.filter(_.newPath == pageName + ".md"),
repository, repository,
isEditable(repository), isEditable(repository),
flash.get("info") flash.get("info")
@@ -142,15 +127,7 @@ trait WikiControllerBase extends ControllerBase {
None, None,
from, from,
to, to,
JGitUtil.getDiffs( JGitUtil.getDiffs(git, Some(from), to, true, false),
git = git,
from = Some(from),
to = to,
fetchContent = true,
makePatch = false,
maxFiles = context.settings.repositoryViewer.maxDiffFiles,
maxLines = context.settings.repositoryViewer.maxDiffLines
),
repository, repository,
isEditable(repository), isEditable(repository),
flash.get("info") flash.get("info")
@@ -159,18 +136,18 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository => get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) { if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
val branch = getWikiBranch(repository.owner, repository.name)
if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, Some(pageName), branch)) { if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, Some(pageName))) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}") redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
} else { } else {
flash.update("info", "This patch was not able to be reversed.") flash.update("info", "This patch was not able to be reversed.")
redirect( redirect(
s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/$from...$to" s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}"
) )
} }
} else Unauthorized() } else Unauthorized()
@@ -178,16 +155,16 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository => get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) { if (isEditable(repository)) {
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
val branch = getWikiBranch(repository.owner, repository.name)
if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, None, branch)) { if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, None)) {
redirect(s"/${repository.owner}/${repository.name}/wiki") redirect(s"/${repository.owner}/${repository.name}/wiki")
} else { } else {
flash.update("info", "This patch was not able to be reversed.") flash.update("info", "This patch was not able to be reversed.")
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/$from...$to") redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
} }
} else Unauthorized() } else Unauthorized()
} }
@@ -196,14 +173,13 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository => get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository =>
if (isEditable(repository)) { if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val branch = getWikiBranch(repository.owner, repository.name) html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName, branch), repository)
} else Unauthorized() } else Unauthorized()
}) })
post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) { if (isEditable(repository)) {
saveWikiPage( saveWikiPage(
repository.owner, repository.owner,
@@ -214,7 +190,8 @@ trait WikiControllerBase extends ControllerBase {
loginAccount, loginAccount,
form.message.getOrElse(""), form.message.getOrElse(""),
Some(form.id) Some(form.id)
).foreach { commitId => ).foreach {
commitId =>
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
val wikiEditInfo = val wikiEditInfo =
EditWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) EditWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
@@ -241,7 +218,8 @@ trait WikiControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) { if (isEditable(repository)) {
saveWikiPage( saveWikiPage(
repository.owner, repository.owner,
@@ -252,7 +230,8 @@ trait WikiControllerBase extends ControllerBase {
loginAccount, loginAccount,
form.message.getOrElse(""), form.message.getOrElse(""),
None None
).foreach { commitId => ).foreach {
commitId =>
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
val createWikiPageInfo = val createWikiPageInfo =
CreateWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName) CreateWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName)
@@ -274,7 +253,8 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository => get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) { if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
deleteWikiPage( deleteWikiPage(
@@ -283,7 +263,7 @@ trait WikiControllerBase extends ControllerBase {
pageName, pageName,
loginAccount.fullName, loginAccount.fullName,
loginAccount.mailAddress, loginAccount.mailAddress,
s"Destroyed $pageName" s"Destroyed ${pageName}"
) )
val deleteWikiInfo = DeleteWikiInfo( val deleteWikiInfo = DeleteWikiInfo(
repository.owner, repository.owner,
@@ -300,8 +280,7 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository => get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
val branch = getWikiBranch(repository.owner, repository.name) html.pages(getWikiPageList(repository.owner, repository.name), repository, isEditable(repository))
html.pages(getWikiPageList(repository.owner, repository.name, branch), repository, isEditable(repository))
}) })
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
@@ -330,29 +309,24 @@ trait WikiControllerBase extends ControllerBase {
value: String, value: String,
params: Map[String, Seq[String]], params: Map[String, Seq[String]],
messages: Messages messages: Messages
): Option[String] = { ): Option[String] =
val owner = params.value("owner") getWikiPageList(params.value("owner"), params.value("repository"))
val repository = params.value("repository")
val branch = getWikiBranch(owner, repository)
getWikiPageList(owner, repository, branch)
.find(_ == value) .find(_ == value)
.map(_ => "Page already exists.") .map(_ => "Page already exists.")
} }
}
private def pageName: Constraint = new Constraint() { private def pagename: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (value.exists("\\/:*?\"<>|".contains(_))) { if (value.exists("\\/:*?\"<>|".contains(_))) {
Some(s"$name contains invalid character.") Some(s"${name} contains invalid character.")
} else if (notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))) { } else if (notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))) {
Some(s"$name starts with invalid character.") Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }
} }
private def notReservedPageName(value: String): Boolean = !(Array[String]("_Sidebar", "_Footer") contains value) private def notReservedPageName(value: String) = !(Array[String]("_Sidebar", "_Footer") contains value)
private def conflictForNew: Constraint = new Constraint() { private def conflictForNew: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
@@ -370,13 +344,7 @@ trait WikiControllerBase extends ControllerBase {
} }
} }
private def targetWikiPage: Option[WikiService.WikiPageInfo] = { private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
val owner = params("owner")
val repository = params("repository")
val pageName = params("pageName")
val branch = getWikiBranch(owner, repository)
getWikiPage(owner, repository, pageName, branch)
}
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
repository.repository.options.wikiOption match { repository.repository.options.wikiOption match {

View File

@@ -15,7 +15,7 @@ import scala.jdk.CollectionConverters._
import scala.util.Using import scala.util.Using
trait ApiGitReferenceControllerBase extends ControllerBase { trait ApiGitReferenceControllerBase extends ControllerBase {
self: ReferrerAuthenticator & WritableUsersAuthenticator => self: ReferrerAuthenticator with WritableUsersAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase]) private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase])
@@ -57,20 +57,17 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iii. Create a reference * iii. Create a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
*/ */
post("/api/v3/repos/:owner/:repository/git/refs")(writableUsersOnly { repository => post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
extractFromJsonBody[CreateARef].map { data => extractFromJsonBody[CreateARef].map {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => data =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
val ref = git.getRepository.findRef(data.ref) val ref = git.getRepository.findRef(data.ref)
if (ref == null) { if (ref == null) {
val update = git.getRepository.updateRef(data.ref) val update = git.getRepository.updateRef(data.ref)
update.setNewObjectId(ObjectId.fromString(data.sha)) update.setNewObjectId(ObjectId.fromString(data.sha))
val result = update.update() val result = update.update()
result match { result match {
case Result.NEW => case Result.NEW => JsonFormat(ApiRef.fromRef(RepositoryName(repository.owner, repository.name), ref))
JsonFormat(
ApiRef
.fromRef(RepositoryName(repository.owner, repository.name), git.getRepository.findRef(data.ref))
)
case _ => UnprocessableEntity(result.name()) case _ => UnprocessableEntity(result.name())
} }
} else { } else {
@@ -86,8 +83,9 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
*/ */
patch("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { repository => patch("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { repository =>
val refName = multiParams("splat").mkString("/") val refName = multiParams("splat").mkString("/")
extractFromJsonBody[UpdateARef].map { data => extractFromJsonBody[UpdateARef].map {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => data =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
val ref = git.getRepository.findRef(refName) val ref = git.getRepository.findRef(refName)
if (ref == null) { if (ref == null) {
UnprocessableEntity("Ref does not exist.") UnprocessableEntity("Ref does not exist.")
@@ -98,7 +96,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
val result = update.update() val result = update.update()
result match { result match {
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE => case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
JsonFormat(ApiRef.fromRef(RepositoryName(repository), git.getRepository.findRef(refName))) JsonFormat(ApiRef.fromRef(RepositoryName(repository), update.getRef))
case _ => UnprocessableEntity(result.name()) case _ => UnprocessableEntity(result.name())
} }
} }

View File

@@ -7,8 +7,13 @@ import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, R
import org.scalatra.ActionResult import org.scalatra.ActionResult
trait ApiIssueCommentControllerBase extends ControllerBase { trait ApiIssueCommentControllerBase extends ControllerBase {
self: AccountService & IssuesService & RepositoryService & HandleCommentService & MilestonesService & self: AccountService
ReadableUsersAuthenticator & ReferrerAuthenticator => with IssuesService
with RepositoryService
with HandleCommentService
with MilestonesService
with ReadableUsersAuthenticator
with ReferrerAuthenticator =>
/* /*
* i. List issue comments for a repository * i. List issue comments for a repository
* https://docs.github.com/en/rest/reference/issues#list-issue-comments-for-a-repository * https://docs.github.com/en/rest/reference/issues#list-issue-comments-for-a-repository
@@ -18,7 +23,8 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
comments = getCommentsForApi(repository.owner, repository.name, issueId) comments = getCommentsForApi(repository.owner, repository.name, issueId)
} yield { } yield {
JsonFormat(comments.map { case (issueComment, user, issue) => JsonFormat(comments.map {
case (issueComment, user, issue) =>
ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest)
}) })
}) getOrElse NotFound() }) getOrElse NotFound()
@@ -79,7 +85,7 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
* iv. Delete a comment * iv. Delete a comment
* https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment * https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment
*/ */
delete("/api/v3/repos/:owner/:repository/issues/comments/:id")(readableUsersOnly { repository => delete("/api/v3/repos/:owner/:repo/issues/comments/:id")(readableUsersOnly { repository =>
val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] = val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] =
for { for {
commentId <- params("id").toIntOpt commentId <- params("id").toIntOpt

View File

@@ -9,8 +9,12 @@ import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, R
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
trait ApiIssueControllerBase extends ControllerBase { trait ApiIssueControllerBase extends ControllerBase {
self: AccountService & IssuesService & IssueCreationService & MilestonesService & ReadableUsersAuthenticator & self: AccountService
ReferrerAuthenticator => with IssuesService
with IssueCreationService
with MilestonesService
with ReadableUsersAuthenticator
with ReferrerAuthenticator =>
/* /*
* i. List issues * i. List issues
* https://developer.github.com/v3/issues/#list-issues * https://developer.github.com/v3/issues/#list-issues
@@ -25,7 +29,7 @@ trait ApiIssueControllerBase extends ControllerBase {
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
// TODO: more api spec condition // TODO: more api spec condition
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
// val baseOwner = getAccountByUserName(repository.owner).get //val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, List[Account])] = val issues: List[(Issue, Account, List[Account])] =
searchIssueByApi( searchIssueByApi(
@@ -35,7 +39,8 @@ trait ApiIssueControllerBase extends ControllerBase {
repos = repository.owner -> repository.name repos = repository.owner -> repository.name
) )
JsonFormat(issues.map { case (issue, issueUser, assigneeUsers) => JsonFormat(issues.map {
case (issue, issueUser, assigneeUsers) =>
ApiIssue( ApiIssue(
issue = issue, issue = issue,
repositoryName = RepositoryName(repository), repositoryName = RepositoryName(repository),

View File

@@ -1,5 +1,5 @@
package gitbucket.core.controller.api package gitbucket.core.controller.api
import gitbucket.core.api.{AddLabelsToAnIssue, ApiError, ApiLabel, CreateALabel, JsonFormat} import gitbucket.core.api.{ApiError, ApiLabel, CreateALabel, JsonFormat}
import gitbucket.core.controller.ControllerBase import gitbucket.core.controller.ControllerBase
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
@@ -7,7 +7,11 @@ import gitbucket.core.util._
import org.scalatra.{Created, NoContent, UnprocessableEntity} import org.scalatra.{Created, NoContent, UnprocessableEntity}
trait ApiIssueLabelControllerBase extends ControllerBase { trait ApiIssueLabelControllerBase extends ControllerBase {
self: AccountService & IssuesService & LabelsService & ReferrerAuthenticator & WritableUsersAuthenticator => self: AccountService
with IssuesService
with LabelsService
with ReferrerAuthenticator
with WritableUsersAuthenticator =>
/* /*
* i. List all labels for this repository * i. List all labels for this repository
@@ -65,7 +69,8 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
data <- extractFromJsonBody[CreateALabel] if data.isValid data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield { } yield {
LockUtil.lock(RepositoryName(repository).fullName) { LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label => getLabel(repository.owner, repository.name, params("labelName")).map {
label =>
if (getLabel(repository.owner, repository.name, data.name).isEmpty) { if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color) updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
JsonFormat( JsonFormat(
@@ -116,10 +121,10 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/ */
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository => post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for { JsonFormat(for {
data <- extractFromJsonBody[AddLabelsToAnIssue] data <- extractFromJsonBody[Seq[String]]
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
} yield { } yield {
data.labels.map { labelName => data.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse( val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel( getLabel(
repository.owner, repository.owner,
@@ -155,11 +160,11 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/ */
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository => put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for { JsonFormat(for {
data <- extractFromJsonBody[AddLabelsToAnIssue] data <- extractFromJsonBody[Seq[String]]
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
} yield { } yield {
deleteAllIssueLabels(repository.owner, repository.name, issueId, true) deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
data.labels.map { labelName => data.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse( val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel( getLabel(
repository.owner, repository.owner,

View File

@@ -7,7 +7,7 @@ import gitbucket.core.util.Implicits._
import org.scalatra.NoContent import org.scalatra.NoContent
trait ApiIssueMilestoneControllerBase extends ControllerBase { trait ApiIssueMilestoneControllerBase extends ControllerBase {
self: MilestonesService & WritableUsersAuthenticator & ReferrerAuthenticator => self: MilestonesService with WritableUsersAuthenticator with ReferrerAuthenticator =>
/* /*
* i. List milestones * i. List milestones
@@ -16,10 +16,8 @@ trait ApiIssueMilestoneControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/milestones")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/milestones")(referrersOnly { repository =>
val state = params.getOrElse("state", "all") val state = params.getOrElse("state", "all")
// TODO "sort", "direction" params should be implemented. // TODO "sort", "direction" params should be implemented.
val apiMilestones = (for ( val apiMilestones = (for (milestoneWithIssue <- getMilestonesWithIssueCount(repository.owner, repository.name)
milestoneWithIssue <- getMilestonesWithIssueCount(repository.owner, repository.name) .sortBy(p => p._1.milestoneId))
.sortBy(p => p._1.milestoneId)
)
yield { yield {
ApiMilestone( ApiMilestone(
repository.repository, repository.repository,

View File

@@ -6,7 +6,7 @@ import gitbucket.core.util.Implicits._
import gitbucket.core.util.{AdminAuthenticator, UsersAuthenticator} import gitbucket.core.util.{AdminAuthenticator, UsersAuthenticator}
trait ApiOrganizationControllerBase extends ControllerBase { trait ApiOrganizationControllerBase extends ControllerBase {
self: RepositoryService & AccountService & AdminAuthenticator & UsersAuthenticator => self: RepositoryService with AccountService with AdminAuthenticator with UsersAuthenticator =>
/* /*
* i. List your organizations * i. List your organizations

View File

@@ -16,8 +16,14 @@ import scala.util.Using
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
trait ApiPullRequestControllerBase extends ControllerBase { trait ApiPullRequestControllerBase extends ControllerBase {
self: AccountService & IssuesService & PullRequestService & RepositoryService & MergeService & ReferrerAuthenticator & self: AccountService
ReadableUsersAuthenticator & WritableUsersAuthenticator => with IssuesService
with PullRequestService
with RepositoryService
with MergeService
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator =>
/* /*
* i. Link Relations * i. Link Relations
@@ -42,7 +48,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
repos = repository.owner -> repository.name repos = repository.owner -> repository.name
) )
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) => JsonFormat(issues.map {
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
ApiPullRequest( ApiPullRequest(
issue = issue, issue = issue,
pullRequest = pullRequest, pullRequest = pullRequest,
@@ -82,7 +89,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
case Left(createPullReq) => case Left(createPullReq) =>
val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReq.head, repository.owner) val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReq.head, repository.owner)
getRepository(reqOwner, repository.name) getRepository(reqOwner, repository.name)
.flatMap { forkedRepository => .flatMap {
forkedRepository =>
getPullRequestCommitFromTo(repository, forkedRepository, createPullReq.base, reqBranch) match { getPullRequestCommitFromTo(repository, forkedRepository, createPullReq.base, reqBranch) match {
case (Some(commitIdFrom), Some(commitIdTo)) => case (Some(commitIdFrom), Some(commitIdTo)) =>
val issueId = insertIssue( val issueId = insertIssue(
@@ -120,7 +128,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
case Right(createPullReqAlt) => case Right(createPullReqAlt) =>
val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReqAlt.head, repository.owner) val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReqAlt.head, repository.owner)
getRepository(reqOwner, repository.name) getRepository(reqOwner, repository.name)
.flatMap { forkedRepository => .flatMap {
forkedRepository =>
getPullRequestCommitFromTo(repository, forkedRepository, createPullReqAlt.base, reqBranch) match { getPullRequestCommitFromTo(repository, forkedRepository, createPullReqAlt.base, reqBranch) match {
case (Some(commitIdFrom), Some(commitIdTo)) => case (Some(commitIdFrom), Some(commitIdTo)) =>
changeIssueToPullRequest(repository.owner, repository.name, createPullReqAlt.issue) changeIssueToPullRequest(repository.owner, repository.name, createPullReqAlt.issue)
@@ -181,8 +190,10 @@ trait ApiPullRequestControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap {
getPullRequest(owner, name, issueId) map { case (issue, pullreq) => issueId =>
getPullRequest(owner, name, issueId) map {
case (issue, pullreq) =>
Using.resource(Git.open(getRepositoryDir(owner, name))) { git => Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
val oldId = git.getRepository.resolve(pullreq.commitIdFrom) val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
val newId = git.getRepository.resolve(pullreq.commitIdTo) val newId = git.getRepository.resolve(pullreq.commitIdTo)
@@ -229,8 +240,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
*/ */
put("/api/v3/repos/:owner/:repository/pulls/:id/merge")(referrersOnly { repository => put("/api/v3/repos/:owner/:repository/pulls/:id/merge")(referrersOnly { repository =>
(for { (for {
// TODO: crash when body is empty //TODO: crash when body is empty
// TODO: Implement sha parameter //TODO: Implement sha parameter
data <- extractFromJsonBody[MergeAPullRequest] data <- extractFromJsonBody[MergeAPullRequest]
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
(issue, pullReq) <- getPullRequest(repository.owner, repository.name, issueId) (issue, pullReq) <- getPullRequest(repository.owner, repository.name, issueId)
@@ -262,7 +273,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
repository, repository,
issueId, issueId,
context.loginAccount.get, context.loginAccount.get,
data.commit_message.getOrElse(""), // TODO: Implement commit_title data.commit_message.getOrElse(""), //TODO: Implement commit_title
strategy, strategy,
pullReq.isDraft, pullReq.isDraft,
context.settings context.settings

View File

@@ -11,7 +11,7 @@ import org.apache.commons.io.FileUtils
import org.scalatra.NoContent import org.scalatra.NoContent
trait ApiReleaseControllerBase extends ControllerBase { trait ApiReleaseControllerBase extends ControllerBase {
self: AccountService & ReleaseService & ReferrerAuthenticator & WritableUsersAuthenticator => self: AccountService with ReleaseService with ReferrerAuthenticator with WritableUsersAuthenticator =>
/** /**
* i. List releases for a repository * i. List releases for a repository
@@ -119,7 +119,8 @@ trait ApiReleaseControllerBase extends ControllerBase {
* ix. Upload a release asset * ix. Upload a release asset
* https://developer.github.com/v3/repos/releases/#upload-a-release-asset * https://developer.github.com/v3/repos/releases/#upload-a-release-asset
*/ */
post("/api/v3/repos/:owner/:repository/releases/:tag/assets")(writableUsersOnly { repository => post("/api/v3/repos/:owner/:repository/releases/:tag/assets")(writableUsersOnly {
repository =>
val name = params("name") val name = params("name")
val tag = params("tag") val tag = params("tag")
getRelease(repository.owner, repository.name, tag) getRelease(repository.owner, repository.name, tag)

View File

@@ -1,19 +1,26 @@
package gitbucket.core.controller.api package gitbucket.core.controller.api
import gitbucket.core.api.* import gitbucket.core.api._
import gitbucket.core.controller.ControllerBase import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{AccountService, ProtectedBranchService, RepositoryService} import gitbucket.core.service.{AccountService, ProtectedBranchService, RepositoryService}
import gitbucket.core.util.* import gitbucket.core.util._
import gitbucket.core.util.Directory.* import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil.{getBranchesNoMergeInfo, processTree} import gitbucket.core.util.JGitUtil.getBranches
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.NoContent import org.scalatra.NoContent
import scala.util.Using import scala.util.Using
trait ApiRepositoryBranchControllerBase extends ControllerBase { trait ApiRepositoryBranchControllerBase extends ControllerBase {
self: RepositoryService & AccountService & OwnerAuthenticator & UsersAuthenticator & GroupManagerAuthenticator & self: RepositoryService
ProtectedBranchService & ReferrerAuthenticator & ReadableUsersAuthenticator & WritableUsersAuthenticator => with AccountService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
with ProtectedBranchService
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator =>
/** /**
* i. List branches * i. List branches
@@ -23,7 +30,11 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
JsonFormat( JsonFormat(
JGitUtil JGitUtil
.getBranchesNoMergeInfo(git) .getBranches(
git = git,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
)
.map { br => .map { br =>
ApiBranchForList(br.name, ApiBranchCommit(br.commitId)) ApiBranchForList(br.name, ApiBranchCommit(br.commitId))
} }
@@ -36,16 +47,19 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
* https://docs.github.com/en/rest/reference/repos#get-a-branch * https://docs.github.com/en/rest/reference/repos#get-a-branch
*/ */
get("/api/v3/repos/:owner/:repository/branches/*")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/branches/*")(referrersOnly { repository =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
(for { (for {
branch <- params.get("splat") if repository.branchList.contains(branch) branch <- params.get("splat") if repository.branchList.contains(branch)
br <- getBranchesNoMergeInfo(git).find(_.name == branch) br <- getBranches(
git,
repository.repository.defaultBranch,
repository.repository.originUserName.isEmpty
).find(_.name == branch)
} yield { } yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat( JsonFormat(
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtectionResponse(protection))( ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtection(protection))(RepositoryName(repository))
RepositoryName(repository)
)
) )
}) getOrElse NotFound() }) getOrElse NotFound()
} }
@@ -60,7 +74,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) { if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat( JsonFormat(
ApiBranchProtectionResponse(protection) ApiBranchProtection(protection)
) )
} else { NotFound() } } else { NotFound() }
}) })
@@ -140,7 +154,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) { if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat( JsonFormat(
ApiBranchProtectionResponse(protection).required_status_checks ApiBranchProtection(protection).required_status_checks
) )
} else { NotFound() } } else { NotFound() }
}) })
@@ -260,29 +274,30 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection * https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/ */
patch("/api/v3/repos/:owner/:repository/branches/*")(ownerOnly { repository => patch("/api/v3/repos/:owner/:repository/branches/*")(ownerOnly { repository =>
import gitbucket.core.api.* import gitbucket.core.api._
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
(for { (for {
branch <- params.get("splat") if repository.branchList.contains(branch) branch <- params.get("splat") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtectionRequest.EnablingAndDisabling].map(_.protection) protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
br <- getBranchesNoMergeInfo(git).find(_.name == branch) br <- getBranches(
git,
repository.repository.defaultBranch,
repository.repository.originUserName.isEmpty
).find(_.name == branch)
} yield { } yield {
if (protection.enabled) { if (protection.enabled) {
enableBranchProtection( enableBranchProtection(
repository.owner, repository.owner,
repository.name, repository.name,
branch, branch,
protection.enforce_admins.getOrElse(false), protection.status.enforcement_level == ApiBranchProtection.Everyone,
protection.required_status_checks.isDefined, protection.status.contexts
protection.required_status_checks.map(_.contexts).getOrElse(Nil),
protection.restrictions.isDefined,
protection.restrictions.map(_.users).getOrElse(Nil)
) )
} else { } else {
disableBranchProtection(repository.owner, repository.name, branch) disableBranchProtection(repository.owner, repository.name, branch)
} }
val response = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch)) JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), protection)(RepositoryName(repository)))
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), response)(RepositoryName(repository)))
}) getOrElse NotFound() }) getOrElse NotFound()
} }
}) })

View File

@@ -7,7 +7,7 @@ import gitbucket.core.util.{OwnerAuthenticator, ReferrerAuthenticator}
import org.scalatra.NoContent import org.scalatra.NoContent
trait ApiRepositoryCollaboratorControllerBase extends ControllerBase { trait ApiRepositoryCollaboratorControllerBase extends ControllerBase {
self: RepositoryService & AccountService & ReferrerAuthenticator & OwnerAuthenticator => self: RepositoryService with AccountService with ReferrerAuthenticator with OwnerAuthenticator =>
/* /*
* i. List repository collaborators * i. List repository collaborators

View File

@@ -4,24 +4,31 @@ import gitbucket.core.controller.ControllerBase
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, CommitsService, ProtectedBranchService} import gitbucket.core.service.{AccountService, CommitsService, ProtectedBranchService}
import gitbucket.core.util.Directory.getRepositoryDir import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.Implicits.* import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil.{CommitInfo, getBranchesNoMergeInfo, getBranchesOfCommit} import gitbucket.core.util.JGitUtil.{CommitInfo, getBranches, getBranchesOfCommit}
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName} import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.revwalk.filter.* import org.eclipse.jgit.revwalk.filter.{
AndRevFilter,
AuthorRevFilter,
CommitTimeRevFilter,
MaxCountRevFilter,
RevFilter,
SkipRevFilter
}
import org.eclipse.jgit.treewalk.filter.{AndTreeFilter, PathFilterGroup, TreeFilter} import org.eclipse.jgit.treewalk.filter.{AndTreeFilter, PathFilterGroup, TreeFilter}
import java.time.format.DateTimeFormatter.*
import java.time.{LocalDateTime, ZoneOffset}
import java.util.Date
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters.* import scala.jdk.CollectionConverters._
import scala.math.min
import scala.util.Using import scala.util.Using
import math.min
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter._
import java.util.Date
import java.time.ZoneOffset
trait ApiRepositoryCommitControllerBase extends ControllerBase { trait ApiRepositoryCommitControllerBase extends ControllerBase {
self: AccountService & CommitsService & ProtectedBranchService & ReferrerAuthenticator => self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator =>
/* /*
* i. List commits on a repository * i. List commits on a repository
* https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
@@ -36,9 +43,11 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
val path = params.get("path").filter(_.nonEmpty) val path = params.get("path").filter(_.nonEmpty)
val since = params.get("since").filter(_.nonEmpty) val since = params.get("since").filter(_.nonEmpty)
val until = params.get("until").filter(_.nonEmpty) val until = params.get("until").filter(_.nonEmpty)
Using.resource(Git.open(getRepositoryDir(owner, name))) { git => Using.resource(Git.open(getRepositoryDir(owner, name))) {
git =>
val repo = git.getRepository val repo = git.getRepository
Using.resource(new RevWalk(repo)) { revWalk => Using.resource(new RevWalk(repo)) {
revWalk =>
val objectId = repo.resolve(sha) val objectId = repo.resolve(sha)
revWalk.markStart(revWalk.parseCommit(objectId)) revWalk.markStart(revWalk.parseCommit(objectId))
if (path.nonEmpty) { if (path.nonEmpty) {
@@ -71,7 +80,8 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
revfilters(0) revfilters(0)
} }
) )
JsonFormat(revWalk.asScala.map { commit => JsonFormat(revWalk.asScala.map {
commit =>
val commitInfo = new CommitInfo(commit) val commitInfo = new CommitInfo(commit)
ApiCommits( ApiCommits(
repositoryName = RepositoryName(repository), repositoryName = RepositoryName(repository),
@@ -95,7 +105,8 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
val name = repository.name val name = repository.name
val sha = params("sha") val sha = params("sha")
Using.resource(Git.open(getRepositoryDir(owner, name))) { git => Using.resource(Git.open(getRepositoryDir(owner, name))) {
git =>
val repo = git.getRepository val repo = git.getRepository
val objectId = repo.resolve(sha) val objectId = repo.resolve(sha)
val commitInfo = Using.resource(new RevWalk(repo)) { revWalk => val commitInfo = Using.resource(new RevWalk(repo)) { revWalk =>
@@ -159,7 +170,7 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val apiBranchForCommits = for { val apiBranchForCommits = for {
branch <- getBranchesOfCommit(git, sha) branch <- getBranchesOfCommit(git, sha)
br <- getBranchesNoMergeInfo(git).find(_.name == branch) br <- getBranches(git, branch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield { } yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
ApiBranchForHeadCommit(branch, ApiBranchCommit(br.commitId), protection.enabled) ApiBranchForHeadCommit(branch, ApiBranchCommit(br.commitId), protection.enabled)

View File

@@ -12,14 +12,15 @@ import org.eclipse.jgit.api.Git
import scala.util.Using import scala.util.Using
trait ApiRepositoryContentsControllerBase extends ControllerBase { trait ApiRepositoryContentsControllerBase extends ControllerBase {
self: ReferrerAuthenticator & WritableUsersAuthenticator & RepositoryCommitFileService => self: ReferrerAuthenticator with WritableUsersAuthenticator with RepositoryCommitFileService =>
/** /**
* i. Get a repository README * i. Get a repository README
* https://docs.github.com/en/rest/reference/repos#get-a-repository-readme * https://docs.github.com/en/rest/reference/repos#get-a-repository-readme
*/ */
get("/api/v3/repos/:owner/:repository/readme")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/readme")(referrersOnly { repository =>
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) {
git =>
val refStr = params.getOrElse("ref", repository.repository.defaultBranch) val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
val files = getFileList(git, refStr, ".", maxFiles = context.settings.repositoryViewer.maxFiles) val files = getFileList(git, refStr, ".", maxFiles = context.settings.repositoryViewer.maxFiles)
files // files should be sorted alphabetically. files // files should be sorted alphabetically.
@@ -133,7 +134,8 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
* requested #2112 * requested #2112
*/ */
put("/api/v3/repos/:owner/:repository/contents/*")(writableUsersOnly { repository => put("/api/v3/repos/:owner/:repository/contents/*")(writableUsersOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount {
loginAccount =>
JsonFormat(for { JsonFormat(for {
data <- extractFromJsonBody[CreateAFile] data <- extractFromJsonBody[CreateAFile]
} yield { } yield {
@@ -144,7 +146,8 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
} }
val paths = multiParams("splat").head.split("/") val paths = multiParams("splat").head.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, path, false)
fileInfo match { fileInfo match {

View File

@@ -15,9 +15,16 @@ import scala.concurrent.duration.Duration
import scala.util.Using import scala.util.Using
trait ApiRepositoryControllerBase extends ControllerBase { trait ApiRepositoryControllerBase extends ControllerBase {
self: RepositoryService & ApiGitReferenceControllerBase & RepositoryCreationService & AccountService & self: RepositoryService
OwnerAuthenticator & UsersAuthenticator & GroupManagerAuthenticator & ReferrerAuthenticator & with ApiGitReferenceControllerBase
ReadableUsersAuthenticator & WritableUsersAuthenticator => with RepositoryCreationService
with AccountService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator =>
/** /**
* i. List your repositories * i. List your repositories
@@ -86,8 +93,7 @@ trait ApiRepositoryControllerBase extends ControllerBase {
data.name, data.name,
data.description, data.description,
data.`private`, data.`private`,
data.auto_init, data.auto_init
context.settings.defaultBranch
) )
Await.result(f, Duration.Inf) Await.result(f, Duration.Inf)
@@ -124,8 +130,7 @@ trait ApiRepositoryControllerBase extends ControllerBase {
data.name, data.name,
data.description, data.description,
data.`private`, data.`private`,
data.auto_init, data.auto_init
context.settings.defaultBranch
) )
Await.result(f, Duration.Inf) Await.result(f, Duration.Inf)
val repository = Database() withTransaction { session => val repository = Database() withTransaction { session =>
@@ -182,7 +187,7 @@ trait ApiRepositoryControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
JsonFormat( JsonFormat(
repository.tags.map(tagInfo => ApiTag(tagInfo.name, RepositoryName(repository), tagInfo.commitId)) self.getRef("tags", repository)
) )
} }
}) })

View File

@@ -7,7 +7,7 @@ import gitbucket.core.util.Implicits._
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, WritableUsersAuthenticator}
trait ApiRepositoryStatusControllerBase extends ControllerBase { trait ApiRepositoryStatusControllerBase extends ControllerBase {
self: AccountService & CommitStatusService & ReferrerAuthenticator & WritableUsersAuthenticator => self: AccountService with CommitStatusService with ReferrerAuthenticator with WritableUsersAuthenticator =>
/* /*
* i. Create a status * i. Create a status
@@ -47,7 +47,8 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase {
ref <- params.get("ref") ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield { } yield {
JsonFormat(getCommitStatusesWithCreator(repository.owner, repository.name, sha).map { case (status, creator) => JsonFormat(getCommitStatusesWithCreator(repository.owner, repository.name, sha).map {
case (status, creator) =>
ApiCommitStatus(status, ApiUser(creator)) ApiCommitStatus(status, ApiUser(creator))
}) })
}) getOrElse NotFound() }) getOrElse NotFound()

View File

@@ -8,7 +8,7 @@ import gitbucket.core.util.Implicits._
import org.scalatra.NoContent import org.scalatra.NoContent
trait ApiRepositoryWebhookControllerBase extends ControllerBase { trait ApiRepositoryWebhookControllerBase extends ControllerBase {
self: RepositoryService & WebHookService & ReferrerAuthenticator & WritableUsersAuthenticator => self: RepositoryService with WebHookService with ReferrerAuthenticator with WritableUsersAuthenticator =>
/* /*
* i. List repository webhooks * i. List repository webhooks

View File

@@ -8,7 +8,7 @@ import gitbucket.core.util.StringUtil._
import org.scalatra.NoContent import org.scalatra.NoContent
trait ApiUserControllerBase extends ControllerBase { trait ApiUserControllerBase extends ControllerBase {
self: RepositoryService & AccountService & AdminAuthenticator & UsersAuthenticator => self: RepositoryService with AccountService with AdminAuthenticator with UsersAuthenticator =>
/** /**
* i. Get a single user * i. Get a single user

View File

@@ -10,7 +10,7 @@ trait AccessTokenComponent { self: Profile =>
val userName = column[String]("USER_NAME") val userName = column[String]("USER_NAME")
val tokenHash = column[String]("TOKEN_HASH") val tokenHash = column[String]("TOKEN_HASH")
val note = column[String]("NOTE") val note = column[String]("NOTE")
def * = (accessTokenId, userName, tokenHash, note).mapTo[AccessToken] def * = (accessTokenId, userName, tokenHash, note).<>(AccessToken.tupled, AccessToken.unapply)
} }
} }
case class AccessToken( case class AccessToken(

View File

@@ -35,7 +35,7 @@ trait AccountComponent { self: Profile =>
groupAccount, groupAccount,
removed, removed,
description.? description.?
).mapTo[Account] ).<>(Account.tupled, Account.unapply)
} }
} }

View File

@@ -9,7 +9,7 @@ trait AccountExtraMailAddressComponent { self: Profile =>
val userName = column[String]("USER_NAME", O PrimaryKey) val userName = column[String]("USER_NAME", O PrimaryKey)
val extraMailAddress = column[String]("EXTRA_MAIL_ADDRESS", O PrimaryKey) val extraMailAddress = column[String]("EXTRA_MAIL_ADDRESS", O PrimaryKey)
def * = def * =
(userName, extraMailAddress).mapTo[AccountExtraMailAddress] (userName, extraMailAddress).<>(AccountExtraMailAddress.tupled, AccountExtraMailAddress.unapply)
} }
} }

View File

@@ -9,7 +9,7 @@ trait AccountFederationComponent { self: Profile =>
val issuer = column[String]("ISSUER") val issuer = column[String]("ISSUER")
val subject = column[String]("SUBJECT") val subject = column[String]("SUBJECT")
val userName = column[String]("USER_NAME") val userName = column[String]("USER_NAME")
def * = (issuer, subject, userName).mapTo[AccountFederation] def * = (issuer, subject, userName).<>(AccountFederation.tupled, AccountFederation.unapply)
def byPrimaryKey(issuer: String, subject: String): Rep[Boolean] = def byPrimaryKey(issuer: String, subject: String): Rep[Boolean] =
(this.issuer === issuer.bind) && (this.subject === subject.bind) (this.issuer === issuer.bind) && (this.subject === subject.bind)

View File

@@ -9,7 +9,7 @@ trait AccountPreferenceComponent { self: Profile =>
val userName = column[String]("USER_NAME", O PrimaryKey) val userName = column[String]("USER_NAME", O PrimaryKey)
val highlighterTheme = column[String]("HIGHLIGHTER_THEME") val highlighterTheme = column[String]("HIGHLIGHTER_THEME")
def * = def * =
(userName, highlighterTheme).mapTo[AccountPreference] (userName, highlighterTheme).<>(AccountPreference.tupled, AccountPreference.unapply)
def byPrimaryKey(userName: String): Rep[Boolean] = this.userName === userName.bind def byPrimaryKey(userName: String): Rep[Boolean] = this.userName === userName.bind
} }

View File

@@ -12,7 +12,7 @@ trait AccountWebHookComponent extends TemplateComponent { self: Profile =>
val url = column[String]("URL") val url = column[String]("URL")
val token = column[Option[String]]("TOKEN") val token = column[Option[String]]("TOKEN")
val ctype = column[WebHookContentType]("CTYPE") val ctype = column[WebHookContentType]("CTYPE")
def * = (userName, url, ctype, token).mapTo[AccountWebHook] def * = (userName, url, ctype, token).<>((AccountWebHook.apply _).tupled, AccountWebHook.unapply)
def byPrimaryKey(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind) def byPrimaryKey(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind)
} }

View File

@@ -12,7 +12,7 @@ trait AccountWebHookEventComponent extends TemplateComponent { self: Profile =>
val url = column[String]("URL") val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT") val event = column[WebHook.Event]("EVENT")
def * = (userName, url, event).mapTo[AccountWebHookEvent] def * = (userName, url, event).<>((AccountWebHookEvent.apply _).tupled, AccountWebHookEvent.unapply)
def byAccountWebHook(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind) def byAccountWebHook(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind)

View File

@@ -11,7 +11,7 @@ trait ActivityComponent extends TemplateComponent { self: Profile =>
lazy val Activities = TableQuery[Activities] lazy val Activities = TableQuery[Activities]
class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
def * : slick.lifted.ProvenShape[Activity] = ??? def * = ???
} }
} }

View File

@@ -3,7 +3,7 @@ package gitbucket.core.model
protected[model] trait TemplateComponent { self: Profile => protected[model] trait TemplateComponent { self: Profile =>
import profile.api._ import profile.api._
trait BasicTemplate { self: Table[?] => trait BasicTemplate { self: Table[_] =>
val userName = column[String]("USER_NAME") val userName = column[String]("USER_NAME")
val repositoryName = column[String]("REPOSITORY_NAME") val repositoryName = column[String]("REPOSITORY_NAME")
@@ -18,7 +18,7 @@ protected[model] trait TemplateComponent { self: Profile =>
(this.userName === userName) && (this.repositoryName === repositoryName) (this.userName === userName) && (this.repositoryName === repositoryName)
} }
trait IssueTemplate extends BasicTemplate { self: Table[?] => trait IssueTemplate extends BasicTemplate { self: Table[_] =>
val issueId = column[Int]("ISSUE_ID") val issueId = column[Int]("ISSUE_ID")
def byIssue(owner: String, repository: String, issueId: Int) = def byIssue(owner: String, repository: String, issueId: Int) =
@@ -28,7 +28,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.issueId === issueId) byRepository(userName, repositoryName) && (this.issueId === issueId)
} }
trait LabelTemplate extends BasicTemplate { self: Table[?] => trait LabelTemplate extends BasicTemplate { self: Table[_] =>
val labelId = column[Int]("LABEL_ID") val labelId = column[Int]("LABEL_ID")
val labelName = column[String]("LABEL_NAME") val labelName = column[String]("LABEL_NAME")
@@ -42,7 +42,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(owner, repository) && (this.labelName === labelName.bind) byRepository(owner, repository) && (this.labelName === labelName.bind)
} }
trait PriorityTemplate extends BasicTemplate { self: Table[?] => trait PriorityTemplate extends BasicTemplate { self: Table[_] =>
val priorityId = column[Int]("PRIORITY_ID") val priorityId = column[Int]("PRIORITY_ID")
val priorityName = column[String]("PRIORITY_NAME") val priorityName = column[String]("PRIORITY_NAME")
@@ -56,7 +56,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(owner, repository) && (this.priorityName === priorityName.bind) byRepository(owner, repository) && (this.priorityName === priorityName.bind)
} }
trait MilestoneTemplate extends BasicTemplate { self: Table[?] => trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
val milestoneId = column[Int]("MILESTONE_ID") val milestoneId = column[Int]("MILESTONE_ID")
def byMilestone(owner: String, repository: String, milestoneId: Int) = def byMilestone(owner: String, repository: String, milestoneId: Int) =
@@ -66,7 +66,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId) byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
} }
trait CommitTemplate extends BasicTemplate { self: Table[?] => trait CommitTemplate extends BasicTemplate { self: Table[_] =>
val commitId = column[String]("COMMIT_ID") val commitId = column[String]("COMMIT_ID")
def byCommit(owner: String, repository: String, commitId: String) = def byCommit(owner: String, repository: String, commitId: String) =
@@ -76,7 +76,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.commitId === commitId) byRepository(userName, repositoryName) && (this.commitId === commitId)
} }
trait BranchTemplate extends BasicTemplate { self: Table[?] => trait BranchTemplate extends BasicTemplate { self: Table[_] =>
val branch = column[String]("BRANCH") val branch = column[String]("BRANCH")
def byBranch(owner: String, repository: String, branchName: String) = def byBranch(owner: String, repository: String, branchName: String) =
byRepository(owner, repository) && (branch === branchName.bind) byRepository(owner, repository) && (branch === branchName.bind)

View File

@@ -8,7 +8,7 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate {
val collaboratorName = column[String]("COLLABORATOR_NAME") val collaboratorName = column[String]("COLLABORATOR_NAME")
val role = column[String]("ROLE") val role = column[String]("ROLE")
def * = (userName, repositoryName, collaboratorName, role).mapTo[Collaborator] def * = (userName, repositoryName, collaboratorName, role).<>(Collaborator.tupled, Collaborator.unapply)
def byPrimaryKey(owner: String, repository: String, collaborator: String) = def byPrimaryKey(owner: String, repository: String, collaborator: String) =
byRepository(owner, repository) && (collaboratorName === collaborator.bind) byRepository(owner, repository) && (collaboratorName === collaborator.bind)

View File

@@ -21,7 +21,7 @@ trait IssueCommentComponent extends TemplateComponent { self: Profile =>
val updatedDate = column[java.util.Date]("UPDATED_DATE") val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = def * =
(userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate)
.mapTo[IssueComment] .<>(IssueComment.tupled, IssueComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
} }
@@ -75,7 +75,7 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile =>
originalCommitId, originalCommitId,
originalOldLine, originalOldLine,
originalNewLine originalNewLine
).mapTo[CommitComment] ).<>(CommitComment.tupled, CommitComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
} }

View File

@@ -30,7 +30,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile =>
creator, creator,
registeredDate, registeredDate,
updatedDate updatedDate
).mapTo[CommitStatus] ).<>((CommitStatus.apply _).tupled, CommitStatus.unapply)
def byPrimaryKey(id: Int) = commitStatusId === id.bind def byPrimaryKey(id: Int) = commitStatusId === id.bind
} }
} }

View File

@@ -5,7 +5,6 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.StringUtil import gitbucket.core.util.StringUtil
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import play.twirl.api.Html
trait CustomFieldComponent extends TemplateComponent { self: Profile => trait CustomFieldComponent extends TemplateComponent { self: Profile =>
import profile.api._ import profile.api._
@@ -16,12 +15,11 @@ trait CustomFieldComponent extends TemplateComponent { self: Profile =>
val fieldId = column[Int]("FIELD_ID", O AutoInc) val fieldId = column[Int]("FIELD_ID", O AutoInc)
val fieldName = column[String]("FIELD_NAME") val fieldName = column[String]("FIELD_NAME")
val fieldType = column[String]("FIELD_TYPE") val fieldType = column[String]("FIELD_TYPE")
val constraints = column[Option[String]]("CONSTRAINTS")
val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES") val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES")
val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS") val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS")
def * = def * =
(userName, repositoryName, fieldId, fieldName, fieldType, constraints, enableForIssues, enableForPullRequests) (userName, repositoryName, fieldId, fieldName, fieldType, enableForIssues, enableForPullRequests)
.mapTo[CustomField] .<>(CustomField.tupled, CustomField.unapply)
def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) = def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) =
(this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.fieldId === fieldId.bind) (this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.fieldId === fieldId.bind)
@@ -33,28 +31,17 @@ case class CustomField(
repositoryName: String, repositoryName: String,
fieldId: Int = 0, fieldId: Int = 0,
fieldName: String, fieldName: String,
fieldType: String, // long, double, string, date, or enum fieldType: String, // long, double, string, or date
constraints: Option[String],
enableForIssues: Boolean, enableForIssues: Boolean,
enableForPullRequests: Boolean enableForPullRequests: Boolean
) )
trait CustomFieldBehavior { trait CustomFieldBehavior {
def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(implicit def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit conext: Context): String
context: Context def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
implicit context: Context
): String ): String
def fieldHtml( def validate(name: String, value: String, messages: Messages): Option[String]
repository: RepositoryInfo,
issueId: Int,
fieldId: Int,
fieldName: String,
constraints: Option[String],
value: String,
editable: Boolean
)(implicit
context: Context
): String
def validate(name: String, constraints: Option[String], value: String, messages: Messages): Option[String]
} }
object CustomFieldBehavior { object CustomFieldBehavior {
@@ -62,7 +49,7 @@ object CustomFieldBehavior {
if (value.isEmpty) None if (value.isEmpty) None
else { else {
CustomFieldBehavior(field.fieldType).flatMap { behavior => CustomFieldBehavior(field.fieldType).flatMap { behavior =>
behavior.validate(field.fieldName, field.constraints, value, messages) behavior.validate(field.fieldName, value, messages)
} }
} }
} }
@@ -73,18 +60,12 @@ object CustomFieldBehavior {
case "double" => Some(DoubleFieldBehavior) case "double" => Some(DoubleFieldBehavior)
case "string" => Some(StringFieldBehavior) case "string" => Some(StringFieldBehavior)
case "date" => Some(DateFieldBehavior) case "date" => Some(DateFieldBehavior)
case "enum" => Some(EnumFieldBehavior)
case _ => None case _ => None
} }
} }
case object LongFieldBehavior extends TextFieldBehavior { case object LongFieldBehavior extends TextFieldBehavior {
override def validate( override def validate(name: String, value: String, messages: Messages): Option[String] = {
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
try { try {
value.toLong value.toLong
None None
@@ -94,12 +75,7 @@ object CustomFieldBehavior {
} }
} }
case object DoubleFieldBehavior extends TextFieldBehavior { case object DoubleFieldBehavior extends TextFieldBehavior {
override def validate( override def validate(name: String, value: String, messages: Messages): Option[String] = {
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
try { try {
value.toDouble value.toDouble
None None
@@ -113,12 +89,7 @@ object CustomFieldBehavior {
private val pattern = "yyyy-MM-dd" private val pattern = "yyyy-MM-dd"
override protected val fieldType: String = "date" override protected val fieldType: String = "date"
override def validate( override def validate(name: String, value: String, messages: Messages): Option[String] = {
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
try { try {
new java.text.SimpleDateFormat(pattern).parse(value) new java.text.SimpleDateFormat(pattern).parse(value)
None None
@@ -129,140 +100,10 @@ object CustomFieldBehavior {
} }
} }
case object EnumFieldBehavior extends CustomFieldBehavior {
override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
implicit context: Context
): String = {
createPulldownHtml(repository, fieldId, fieldName, constraints, None, None)
}
override def fieldHtml(
repository: RepositoryInfo,
issueId: Int,
fieldId: Int,
fieldName: String,
constraints: Option[String],
value: String,
editable: Boolean
)(implicit context: Context): String = {
if (!editable) {
val sb = new StringBuilder
sb.append("""</div>""")
sb.append("""<div>""")
if (value == "") {
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">No ${StringUtil.escapeHtml(
fieldName
)}</span></span>""")
} else {
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">${StringUtil
.escapeHtml(value)}</span></span>""")
}
sb.toString()
} else {
createPulldownHtml(repository, fieldId, fieldName, constraints, Some(issueId), Some(value))
}
}
private def createPulldownHtml(
repository: RepositoryInfo,
fieldId: Int,
fieldName: String,
constraints: Option[String],
issueId: Option[Int],
value: Option[String]
)(implicit context: Context): String = {
val sb = new StringBuilder
sb.append("""<div class="pull-right">""")
sb.append(
gitbucket.core.helper.html
.dropdown("Edit", right = true, filter = (fieldName, s"Filter $fieldName")) {
val options = new StringBuilder()
options.append(
s"""<li><a href="javascript:void(0);" class="custom-field-option-$fieldId" data-value=""><i class="octicon octicon-x"></i> Clear ${StringUtil
.escapeHtml(fieldName)}</a></li>"""
)
constraints.foreach { x =>
x.split(",").map(_.trim).foreach { item =>
options.append(s"""<li>
| <a href="javascript:void(0);" class="custom-field-option-$fieldId" data-value="${StringUtil
.escapeHtml(item)}">
| ${gitbucket.core.helper.html.checkicon(value.contains(item))}
| ${StringUtil.escapeHtml(item)}
| </a>
|</li>
|""".stripMargin)
}
}
Html(options.toString())
}
.toString()
)
sb.append("""</div>""")
sb.append("""</div>""")
sb.append("""<div>""")
value match {
case None =>
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">No ${StringUtil.escapeHtml(
fieldName
)}</span></span>""")
case Some(value) =>
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">${StringUtil
.escapeHtml(value)}</span></span>""")
}
if (value.isEmpty || issueId.isEmpty) {
sb.append(s"""<input type="hidden" id="custom-field-$fieldId" name="custom-field-$fieldId" value=""/>""")
sb.append(s"""<script>
|$$('a.custom-field-option-$fieldId').click(function(){
| const value = $$(this).data('value');
| $$('a.custom-field-option-$fieldId i.octicon-check').removeClass('octicon-check');
| $$('#custom-field-$fieldId').val(value);
| if (value == '') {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text('No ${StringUtil
.escapeHtml(fieldName)}'));
| } else {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text(value));
| $$('a.custom-field-option-$fieldId[data-value=' + value + '] i').addClass('octicon-check');
| }
|});
|</script>""".stripMargin)
} else {
sb.append(s"""<script>
|$$('a.custom-field-option-$fieldId').click(function(){
| const value = $$(this).data('value');
| $$.post('${helpers.url(repository)}/issues/${issueId.get}/customfield/$fieldId',
| { value: value },
| function(data){
| $$('a.custom-field-option-$fieldId i.octicon-check').removeClass('octicon-check');
| if (value == '') {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text('No ${StringUtil
.escapeHtml(fieldName)}'));
| } else {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text(value));
| $$('a.custom-field-option-$fieldId[data-value=' + value + '] i').addClass('octicon-check');
| }
| }
| );
|});
|</script>
|""".stripMargin)
}
sb.toString()
}
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = None
}
trait TextFieldBehavior extends CustomFieldBehavior { trait TextFieldBehavior extends CustomFieldBehavior {
protected val fieldType = "text" protected val fieldType = "text"
override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])( def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit context: Context): String = {
implicit context: Context
): String = {
val sb = new StringBuilder val sb = new StringBuilder
sb.append( sb.append(
s"""<input type="$fieldType" class="form-control input-sm" id="custom-field-$fieldId" name="custom-field-$fieldId" data-field-id="$fieldId" style="width: 120px;"/>""" s"""<input type="$fieldType" class="form-control input-sm" id="custom-field-$fieldId" name="custom-field-$fieldId" data-field-id="$fieldId" style="width: 120px;"/>"""
@@ -270,7 +111,8 @@ object CustomFieldBehavior {
sb.append(s"""<script> sb.append(s"""<script>
|$$('#custom-field-$fieldId').focusout(function(){ |$$('#custom-field-$fieldId').focusout(function(){
| const $$this = $$(this); | const $$this = $$(this);
| $$.post('${helpers.url(repository)}/issues/customfield_validation/$fieldId', | const fieldId = $$this.data('field-id');
| $$.post('${helpers.url(repository)}/issues/customfield_validation/' + fieldId,
| { value: $$this.val() }, | { value: $$this.val() },
| function(data){ | function(data){
| if (data != '') { | if (data != '') {
@@ -286,34 +128,14 @@ object CustomFieldBehavior {
sb.toString() sb.toString()
} }
override def fieldHtml( def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
repository: RepositoryInfo, implicit context: Context
issueId: Int,
fieldId: Int,
fieldName: String,
constraints: Option[String],
value: String,
editable: Boolean
)(implicit
context: Context
): String = { ): String = {
val sb = new StringBuilder val sb = new StringBuilder
if (value.nonEmpty) {
sb.append( sb.append(
s"""<span id="custom-field-$fieldId-label" class="custom-field-label">${StringUtil s"""<span id="custom-field-$fieldId-label" class="custom-field-label">${StringUtil
.escapeHtml(value)}</span>""" .escapeHtml(value)}</span>""".stripMargin
) )
} else {
if (editable) {
sb.append(
s"""<span id="custom-field-$fieldId-label" class="custom-field-label"><i class="octicon octicon-pencil" style="cursor: pointer;"></i></span>"""
)
} else {
sb.append(
s"""<span id="custom-field-$fieldId-label" class="custom-field-label">N/A</span>"""
)
}
}
if (editable) { if (editable) {
sb.append( sb.append(
s"""<input type="$fieldType" id="custom-field-$fieldId-editor" class="form-control input-sm custom-field-editor" data-field-id="$fieldId" style="width: 120px; display: none;"/>""" s"""<input type="$fieldType" id="custom-field-$fieldId-editor" class="form-control input-sm custom-field-editor" data-field-id="$fieldId" style="width: 120px; display: none;"/>"""
@@ -327,23 +149,20 @@ object CustomFieldBehavior {
| |
|$$('#custom-field-$fieldId-editor').focusout(function(){ |$$('#custom-field-$fieldId-editor').focusout(function(){
| const $$this = $$(this); | const $$this = $$(this);
| $$.post('${helpers.url(repository)}/issues/customfield_validation/$fieldId', | const fieldId = $$this.data('field-id');
| $$.post('${helpers.url(repository)}/issues/customfield_validation/' + fieldId,
| { value: $$this.val() }, | { value: $$this.val() },
| function(data){ | function(data){
| if (data != '') { | if (data != '') {
| $$('#custom-field-$fieldId-error').text(data); | $$('#custom-field-$fieldId-error').text(data);
| } else { | } else {
| $$('#custom-field-$fieldId-error').text(''); | $$('#custom-field-$fieldId-error').text('');
| $$.post('${helpers.url(repository)}/issues/$issueId/customfield/$fieldId', | $$.post('${helpers.url(repository)}/issues/$issueId/customfield/' + fieldId,
| { value: $$this.val() }, | { value: $$this.val() },
| function(data){ | function(data){
| $$this.hide(); | $$this.hide();
| if (data == '') {
| $$this.prev().html('<i class="octicon octicon-pencil" style="cursor: pointer;">').show();
| } else {
| $$this.prev().text(data).show(); | $$this.prev().text(data).show();
| } | }
| }
| ); | );
| } | }
| } | }
@@ -367,11 +186,6 @@ object CustomFieldBehavior {
sb.toString() sb.toString()
} }
override def validate( def validate(name: String, value: String, messages: Messages): Option[String] = None
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = None
} }
} }

View File

@@ -11,7 +11,7 @@ trait DeployKeyComponent extends TemplateComponent { self: Profile =>
val publicKey = column[String]("PUBLIC_KEY") val publicKey = column[String]("PUBLIC_KEY")
val allowWrite = column[Boolean]("ALLOW_WRITE") val allowWrite = column[Boolean]("ALLOW_WRITE")
def * = def * =
(userName, repositoryName, deployKeyId, title, publicKey, allowWrite).mapTo[DeployKey] (userName, repositoryName, deployKeyId, title, publicKey, allowWrite).<>(DeployKey.tupled, DeployKey.unapply)
def byPrimaryKey(userName: String, repositoryName: String, deployKeyId: Int) = def byPrimaryKey(userName: String, repositoryName: String, deployKeyId: Int) =
(this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.deployKeyId === deployKeyId.bind) (this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.deployKeyId === deployKeyId.bind)

View File

@@ -11,7 +11,7 @@ trait GpgKeyComponent { self: Profile =>
val gpgKeyId = column[Long]("GPG_KEY_ID") val gpgKeyId = column[Long]("GPG_KEY_ID")
val title = column[String]("TITLE") val title = column[String]("TITLE")
val publicKey = column[String]("PUBLIC_KEY") val publicKey = column[String]("PUBLIC_KEY")
def * = (userName, keyId, gpgKeyId, title, publicKey).mapTo[GpgKey] def * = (userName, keyId, gpgKeyId, title, publicKey).<>(GpgKey.tupled, GpgKey.unapply)
def byPrimaryKey(userName: String, keyId: Int) = def byPrimaryKey(userName: String, keyId: Int) =
(this.userName === userName.bind) && (this.keyId === keyId.bind) (this.userName === userName.bind) && (this.keyId === keyId.bind)

View File

@@ -9,7 +9,7 @@ trait GroupMemberComponent { self: Profile =>
val groupName = column[String]("GROUP_NAME", O PrimaryKey) val groupName = column[String]("GROUP_NAME", O PrimaryKey)
val userName = column[String]("USER_NAME", O PrimaryKey) val userName = column[String]("USER_NAME", O PrimaryKey)
val isManager = column[Boolean]("MANAGER") val isManager = column[Boolean]("MANAGER")
def * = (groupName, userName, isManager).mapTo[GroupMember] def * = (groupName, userName, isManager).<>(GroupMember.tupled, GroupMember.unapply)
} }
} }

View File

@@ -47,7 +47,7 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
registeredDate, registeredDate,
updatedDate, updatedDate,
pullRequest pullRequest
).mapTo[Issue] ).<>(Issue.tupled, Issue.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
} }

View File

@@ -9,7 +9,8 @@ trait IssueAssigneeComponent extends TemplateComponent { self: Profile =>
class IssueAssignees(tag: Tag) extends Table[IssueAssignee](tag, "ISSUE_ASSIGNEE") with IssueTemplate { class IssueAssignees(tag: Tag) extends Table[IssueAssignee](tag, "ISSUE_ASSIGNEE") with IssueTemplate {
val assigneeUserName = column[String]("ASSIGNEE_USER_NAME") val assigneeUserName = column[String]("ASSIGNEE_USER_NAME")
def * = def * =
(userName, repositoryName, issueId, assigneeUserName).mapTo[IssueAssignee] (userName, repositoryName, issueId, assigneeUserName)
.<>(IssueAssignee.tupled, IssueAssignee.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, assigneeUserName: String) = { def byPrimaryKey(owner: String, repository: String, issueId: Int, assigneeUserName: String) = {
byIssue(owner, repository, issueId) && this.assigneeUserName === assigneeUserName.bind byIssue(owner, repository, issueId) && this.assigneeUserName === assigneeUserName.bind

View File

@@ -13,7 +13,8 @@ trait IssueCustomFieldComponent extends TemplateComponent { self: Profile =>
val fieldId = column[Int]("FIELD_ID", O.PrimaryKey) val fieldId = column[Int]("FIELD_ID", O.PrimaryKey)
val value = column[String]("VALUE") val value = column[String]("VALUE")
def * = def * =
(userName, repositoryName, issueId, fieldId, value).mapTo[IssueCustomField] (userName, repositoryName, issueId, fieldId, value)
.<>(IssueCustomField.tupled, IssueCustomField.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, fieldId: Int) = { def byPrimaryKey(owner: String, repository: String, issueId: Int, fieldId: Int) = {
this.userName === owner.bind && this.repositoryName === repository.bind && this.issueId === issueId.bind && this.fieldId === fieldId.bind this.userName === owner.bind && this.repositoryName === repository.bind && this.issueId === issueId.bind && this.fieldId === fieldId.bind

View File

@@ -6,7 +6,7 @@ trait IssueLabelComponent extends TemplateComponent { self: Profile =>
lazy val IssueLabels = TableQuery[IssueLabels] lazy val IssueLabels = TableQuery[IssueLabels]
class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate { class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate {
def * = (userName, repositoryName, issueId, labelId).mapTo[IssueLabel] def * = (userName, repositoryName, issueId, labelId).<>(IssueLabel.tupled, IssueLabel.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) = def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
byIssue(owner, repository, issueId) && (this.labelId === labelId.bind) byIssue(owner, repository, issueId) && (this.labelId === labelId.bind)
} }

View File

@@ -9,7 +9,7 @@ trait LabelComponent extends TemplateComponent { self: Profile =>
override val labelId = column[Int]("LABEL_ID", O AutoInc) override val labelId = column[Int]("LABEL_ID", O AutoInc)
override val labelName = column[String]("LABEL_NAME") override val labelName = column[String]("LABEL_NAME")
val color = column[String]("COLOR") val color = column[String]("COLOR")
def * = (userName, repositoryName, labelId, labelName, color).mapTo[Label] def * = (userName, repositoryName, labelId, labelName, color).<>(Label.tupled, Label.unapply)
def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], labelId: Rep[Int]) = def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], labelId: Rep[Int]) =

View File

@@ -13,7 +13,8 @@ trait MilestoneComponent extends TemplateComponent { self: Profile =>
val dueDate = column[Option[java.util.Date]]("DUE_DATE") val dueDate = column[Option[java.util.Date]]("DUE_DATE")
val closedDate = column[Option[java.util.Date]]("CLOSED_DATE") val closedDate = column[Option[java.util.Date]]("CLOSED_DATE")
def * = def * =
(userName, repositoryName, milestoneId, title, description, dueDate, closedDate).mapTo[Milestone] (userName, repositoryName, milestoneId, title, description, dueDate, closedDate)
.<>(Milestone.tupled, Milestone.unapply)
def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], milestoneId: Rep[Int]) = def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], milestoneId: Rep[Int]) =

View File

@@ -13,7 +13,8 @@ trait PriorityComponent extends TemplateComponent { self: Profile =>
val isDefault = column[Boolean]("IS_DEFAULT") val isDefault = column[Boolean]("IS_DEFAULT")
val color = column[String]("COLOR") val color = column[String]("COLOR")
def * = def * =
(userName, repositoryName, priorityId, priorityName, description.?, isDefault, ordering, color).mapTo[Priority] (userName, repositoryName, priorityId, priorityName, description.?, isDefault, ordering, color)
.<>(Priority.tupled, Priority.unapply)
def byPrimaryKey(owner: String, repository: String, priorityId: Int) = byPriority(owner, repository, priorityId) def byPrimaryKey(owner: String, repository: String, priorityId: Int) = byPriority(owner, repository, priorityId)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) = def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) =

View File

@@ -1,18 +1,16 @@
package gitbucket.core.model package gitbucket.core.model
trait ProtectedBranchComponent extends TemplateComponent { self: Profile => trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
import profile.api.* import profile.api._
import self._
lazy val ProtectedBranches = TableQuery[ProtectedBranches] lazy val ProtectedBranches = TableQuery[ProtectedBranches]
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate { class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") // enforceAdmins val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN")
val requiredStatusCheck = column[Boolean]("REQUIRED_STATUS_CHECK") def * = (userName, repositoryName, branch, statusCheckAdmin).<>(ProtectedBranch.tupled, ProtectedBranch.unapply)
val restrictions = column[Boolean]("RESTRICTIONS") def byPrimaryKey(userName: String, repositoryName: String, branch: String) =
def * =
(userName, repositoryName, branch, statusCheckAdmin, requiredStatusCheck, restrictions).mapTo[ProtectedBranch]
def byPrimaryKey(userName: String, repositoryName: String, branch: String): Rep[Boolean] =
byBranch(userName, repositoryName, branch) byBranch(userName, repositoryName, branch)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]): Rep[Boolean] = def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) =
byBranch(userName, repositoryName, branch) byBranch(userName, repositoryName, branch)
} }
@@ -22,29 +20,10 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
with BranchTemplate { with BranchTemplate {
val context = column[String]("CONTEXT") val context = column[String]("CONTEXT")
def * = def * =
(userName, repositoryName, branch, context).mapTo[ProtectedBranchContext] (userName, repositoryName, branch, context).<>(ProtectedBranchContext.tupled, ProtectedBranchContext.unapply)
}
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( case class ProtectedBranch(userName: String, repositoryName: String, branch: String, statusCheckAdmin: Boolean)
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 ProtectedBranchContext(userName: String, repositoryName: String, branch: String, context: String)
case class ProtectedBranchRestriction(userName: String, repositoryName: String, branch: String, allowedUser: String)

Some files were not shown because too many files have changed in this diff Show More