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
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
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
java: [17, 21]
java: [8, 11, 17]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v3
env:
cache-name: cache-sbt-libs
with:
@@ -23,20 +22,18 @@ jobs:
~/.cache/coursier/v1
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.java }}
distribution: adopt
- name: Setup sbt launcher
uses: sbt/setup-sbt@v1
- name: Run tests
run: sbt scalafmtSbtCheck scalafmtCheckAll test
run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
- name: Scala 3
run: sbt '++ 3.x' update # TODO
run: sbt '++ 3.1.2!' update # TODO
- name: Build executable
run: sbt executable
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
path: ./target/executable/gitbucket.*

1
.gitignore vendored
View File

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

View File

@@ -3,6 +3,7 @@ updates.limit = 3
updates.includeScala = true
updates.pin = [
{ groupId = "org.eclipse.jetty", version = "10." }
{ groupId = "org.mariadb.jdbc", version = "2." }
{ groupId = "org.eclipse.jetty", version = "9." }
{ 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
maxColumn = 120
docstrings.style = keep
docstrings = JavaDoc
align.tokens = ["%", "%%", {code = "=>", owner = "Case"}]
align.openParenCallSite = false
align.openParenDefnSite = false
continuationIndent.callSite = 2
continuationIndent.defnSite = 2
danglingParentheses.preset = 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
danglingParentheses = true

View File

@@ -1,66 +1,6 @@
# Changelog
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
- Fix several issues around multiple assignees in issues and pull requests
- 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:
@@ -24,7 +24,7 @@ The current version of GitBucket provides many features such as:
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`.
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 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.
What's New in 4.43.x
What's New in 4.38.x
-------------
## 4.43.0 - 29 Jun 2025
- Upgrade H2 database from 1.x to 2.x
## 4.38.3 - 30 Oct 2022
- 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:
```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
## 4.38.1 - 10 Sep 2022
- Fix comment diff in Chrome 105
- Fix Markdown table CSS
- Fix HTML rendering of multiple asignees
# 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
```
## 4.38.0 - 3 Sep 2022
- Support multiple assignees for Issues and Pull requests
- 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`.
```
db {
url = "jdbc:h2:${DatabaseHome};MVCC=true" // => "jdbc:h2:${DatabaseHome}"
...
}
```
See the [change log](CHANGELOG.md) for all the past updates.
See the [change log](CHANGELOG.md) for all of the updates.

184
build.sbt
View File

@@ -1,76 +1,85 @@
import com.typesafe.sbt.license.{DepModuleInfo, LicenseInfo}
import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.43.0"
val ScalatraVersion = "3.1.2"
val JettyVersion = "10.0.25"
val JgitVersion = "6.10.1.202505221210-r"
val GitBucketVersion = "4.38.3"
val ScalatraVersion = "2.8.4"
val JettyVersion = "9.4.49.v20220914"
val JgitVersion = "5.13.1.202206130422-r"
lazy val root = (project in file("."))
.enablePlugins(SbtTwirl, ContainerPlugin)
.enablePlugins(SbtTwirl, ScalatraPlugin)
sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.13.16"
crossScalaVersions += "3.7.2"
scalaVersion := "2.13.10"
// scalafmtOnCompile := true
coverageExcludedPackages := ".*\\.html\\..*"
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % JgitVersion,
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % JgitVersion,
"org.scalatra" %% "scalatra-javax" % ScalatraVersion,
"org.scalatra" %% "scalatra-json-javax" % ScalatraVersion,
"org.scalatra" %% "scalatra-forms-javax" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "4.1.0-M8",
"commons-io" % "commons-io" % "2.20.0",
"io.github.gitbucket" % "solidbase" % "1.1.0",
"io.github.gitbucket" % "markedj" % "1.0.20",
"org.tukaani" % "xz" % "1.10",
"org.apache.commons" % "commons-compress" % "1.28.0",
"org.apache.commons" % "commons-email" % "1.6.0",
"commons-net" % "commons-net" % "3.12.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.14",
"org.apache.sshd" % "apache-sshd" % "2.15.0" exclude ("org.slf4j", "slf4j-jdk14") exclude (
"org.apache.sshd",
"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.h2database" % "h2" % "2.3.232",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.12",
"org.postgresql" % "postgresql" % "42.7.7",
"ch.qos.logback" % "logback-classic" % "1.5.18",
"com.zaxxer" % "HikariCP" % "7.0.1" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.4",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.16",
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.20",
"com.github.zafarkhaja" % "java-semver" % "0.10.2",
"com.nimbusds" % "oauth2-oidc-sdk" % "11.27",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.13.2" % "test",
"org.scalatra" %% "scalatra-scalatest-javax" % ScalatraVersion % "test",
"org.mockito" % "mockito-core" % "5.18.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.43.0" % "test",
"org.testcontainers" % "mysql" % "1.21.3" % "test",
"org.testcontainers" % "postgresql" % "1.21.3" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "1.1.1",
"org.kohsuke" % "github-api" % "1.329" % "test"
// dependency settings
resolvers ++= Seq(
Classpaths.typesafeReleases,
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
)
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % JgitVersion,
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % JgitVersion,
"org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.0.6" cross CrossVersion.for3Use2_13,
"commons-io" % "commons-io" % "2.11.0",
"io.github.gitbucket" % "solidbase" % "1.0.5",
"io.github.gitbucket" % "markedj" % "1.0.17",
"org.apache.commons" % "commons-compress" % "1.21",
"org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.8.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
"org.apache.sshd" % "apache-sshd" % "2.9.1" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "2.5.0",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "3.0.8",
"org.postgresql" % "postgresql" % "42.5.0",
"ch.qos.logback" % "logback-classic" % "1.2.11",
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.2",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.12",
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.18",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "10.1",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.13.2" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
"org.mockito" % "mockito-core" % "4.8.1" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.40.11" % "test",
"org.testcontainers" % "mysql" % "1.17.5" % "test",
"org.testcontainers" % "postgresql" % "1.17.5" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.3.0",
"org.kohsuke" % "github-api" % "1.313" % "test"
)
libraryDependencies ~= {
_.map {
case x if x.name == "twirl-api" =>
x cross CrossVersion.for3Use2_13
case x =>
x
}
}
// Compiler settings
scalacOptions := Seq(
"-deprecation",
@@ -80,16 +89,8 @@ scalacOptions := Seq(
"-Wunused:imports",
"-Wconf:cat=unused&src=twirl/.*:s,cat=unused&src=scala/gitbucket/core/model/[^/]+\\.scala:s"
)
scalacOptions ++= {
scalaBinaryVersion.value match {
case "2.13" =>
Seq("-Xsource:3-cross")
case _ =>
Nil
}
}
compile / javacOptions ++= Seq("-target", "11", "-source", "11")
Container / javaOptions += "-Dlogback.configurationFile=/logback-dev.xml"
compile / javacOptions ++= Seq("-target", "8", "-source", "8")
Jetty / javaOptions += "-Dlogback.configurationFile=/logback-dev.xml"
// Test settings
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
@@ -113,8 +114,8 @@ assembly / assemblyMergeStrategy := {
// Exclude a war file from published artifacts
signedArtifacts := {
signedArtifacts.value.filterNot { case (_, file) =>
file.getName.endsWith(".war") || file.getName.endsWith(".war.asc")
signedArtifacts.value.filterNot {
case (_, file) => file.getName.endsWith(".war") || file.getName.endsWith(".war.asc")
}
}
@@ -122,14 +123,15 @@ signedArtifacts := {
val ExecutableConfig = config("executable").hide
Keys.ivyConfigurations += ExecutableConfig
libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
"org.eclipse.jetty" % "jetty-security" % 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-xml" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
)
// Run package task before test to generate target/webapp for integration test
@@ -156,11 +158,9 @@ executableKey := {
// include jetty classes
val jettyJars = Keys.update.value select configurationFilter(name = ExecutableConfig.name)
jettyJars foreach { jar =>
IO unzip (
jar,
temp,
(name: String) => (name startsWith "javax/") || (name startsWith "org/") || (name startsWith "META-INF/services/")
)
IO unzip (jar, temp, (name: String) =>
(name startsWith "javax/") ||
(name startsWith "org/"))
}
// include original war file
@@ -185,7 +185,7 @@ executableKey := {
val url = "https://github.com/" +
s"gitbucket/gitbucket-${pluginId}-plugin/releases/download/${pluginVersion}/gitbucket-${pluginId}-plugin-${pluginVersion}.jar"
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 _ => ()
}
}
@@ -206,9 +206,10 @@ executableKey := {
"md5" -> "MD5",
"sha1" -> "SHA-1",
"sha256" -> "SHA-256"
).foreach { case (extension, algorithm) =>
val checksumFile = workDir / (warName + "." + extension)
Checksums generate (outputFile, checksumFile, algorithm)
).foreach {
case (extension, algorithm) =>
val checksumFile = workDir / (warName + "." + extension)
Checksums generate (outputFile, checksumFile, algorithm)
}
// done
@@ -216,9 +217,9 @@ executableKey := {
outputFile
}
publishTo := {
val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/"
if (isSnapshot.value) Some("central-snapshots" at centralSnapshots)
else localStaging.value
val nexus = "https://oss.sonatype.org/"
if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
publishMavenStyle := true
pomIncludeRepository := { _ =>
@@ -284,12 +285,7 @@ Test / testOptions ++= {
}
}
Container / javaOptions ++= Seq(
"-Dlogback.configurationFile=/logback-dev.xml",
Jetty / javaOptions ++= Seq(
"-Xdebug",
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000",
"-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF",
// "-Ddev-features=keep-session"
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
)
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
========
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
$ 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.
```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`.
Source code modifications are detected and a reloading happens automatically.
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.
Source code modifications are detected and a reloading happens automatically. You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
Build war file
--------
@@ -51,8 +37,7 @@ To build an executable war file, run
$ sbt executable
```
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`.
We release this war file as release artifact.
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
Run tests spec
---------

View File

@@ -1,6 +1,6 @@
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
javaOptions in Jetty ++= Seq(
@@ -12,7 +12,7 @@ javaOptions in Jetty ++= Seq(
Run GitBucket:
```shell
$ sbt ~container:start
$ sbt ~jetty:start
```
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:
```
* /HOME/.gitbucket
* gitbucket.conf
* database.conf
* activity.log
* data.mv.db, data.trace.db (H2 data files if the default embed H2 is used)
* /HOME/gitbucket
* /repositories
* /USER_NAME
* /REPO_NAME.git (substance of repository. GitServlet sees this directory)
* /REPO_NAME.wiki.git (wiki repository)
* /REPO_NAME
* /issues (files attached to issue)
* /issues (files which are attached to issue)
* /lfs (LFS managed files)
* /data
* /USER_NAME
@@ -24,8 +20,6 @@ This directory has following structure:
* /plugins
* plugin.jar
* /.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
* /_upload
* /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.
First, start the sbt shell:
First, hit following command to publish artifacts to the sonatype OSS repository:
```bash
$ 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
$ 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
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.
### Make release war file
@@ -66,4 +55,5 @@ Run `sbt executable`. The release war file and fingerprint are generated into `t
```bash
$ sbt executable
```
Create new release from the corresponded tag on GitHub, then upload generated jar file and fingerprints to the release.

View File

@@ -1,6 +1,6 @@
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:

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")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5")
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.9")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.7.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.0.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.0")
addDependencyTreePlugin

View File

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

View File

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

View File

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

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"/>
<!--================================================================================================-->
<!-- ISSUE_LABEL -->
<!-- ISSUE_ID -->
<!--================================================================================================-->
<createTable tableName="ISSUE_LABEL">
<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.sql.Connection
import java.util.UUID
import gitbucket.core.model.Activity
import gitbucket.core.util.Directory.ActivityLog
import gitbucket.core.util.JDBCUtil
@@ -14,7 +15,6 @@ import org.json4s.{Formats, NoTypeHints}
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
import java.util.logging.Level
import scala.util.Using
object GitBucketCoreModule
@@ -76,20 +76,21 @@ object GitBucketCoreModule
import JDBCUtil._
val conn = context.get(Solidbase.CONNECTION).asInstanceOf[Connection]
val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") { rs =>
Activity(
activityId = UUID.randomUUID().toString,
userName = rs.getString("USER_NAME"),
repositoryName = rs.getString("REPOSITORY_NAME"),
activityUserName = rs.getString("ACTIVITY_USER_NAME"),
activityType = rs.getString("ACTIVITY_TYPE"),
message = rs.getString("MESSAGE"),
additionalInfo = {
val additionalInfo = rs.getString("ADDITIONAL_INFO")
if (rs.wasNull()) None else Some(additionalInfo)
},
activityDate = rs.getTimestamp("ACTIVITY_DATE")
)
val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") {
rs =>
Activity(
activityId = UUID.randomUUID().toString,
userName = rs.getString("USER_NAME"),
repositoryName = rs.getString("REPOSITORY_NAME"),
activityUserName = rs.getString("ACTIVITY_USER_NAME"),
activityType = rs.getString("ACTIVITY_TYPE"),
message = rs.getString("MESSAGE"),
additionalInfo = {
val additionalInfo = rs.getString("ADDITIONAL_INFO")
if (rs.wasNull()) None else Some(additionalInfo)
},
activityDate = rs.getTimestamp("ACTIVITY_DATE")
)
}
Using.resource(new FileOutputStream(ActivityLog, true)) { out =>
list.foreach { activity =>
@@ -113,15 +114,5 @@ object GitBucketCoreModule
new Version("4.38.0", new LiquibaseMigration("update/gitbucket-core_4.38.xml")),
new Version("4.38.1"),
new Version("4.38.2"),
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)
}
new Version("4.38.3")
)

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/#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
) extends FieldSerializable {
val _links =

View File

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

View File

@@ -29,7 +29,7 @@ case class ApiCommit(
object 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(
id = commit.id,
message = commit.fullMessage,

View File

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

View File

@@ -22,15 +22,16 @@ object ApiContents {
ApiContents("dir", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName)
} else {
content
.map(arr =>
ApiContents(
"file",
fileInfo.name,
fileInfo.path,
fileInfo.id.getName,
Some(Base64.getEncoder.encodeToString(arr)),
Some("base64")
)(repositoryName)
.map(
arr =>
ApiContents(
"file",
fileInfo.name,
fileInfo.path,
fileInfo.id.getName,
Some(Base64.getEncoder.encodeToString(arr)),
Some("base64")
)(repositoryName)
)
.getOrElse(ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName))
}

View File

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

View File

@@ -27,10 +27,10 @@ case class ApiPullRequest(
val id = 0 // dummy id
val assignee = assignees.headOption
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 patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
//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 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 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}")
@@ -69,8 +69,7 @@ object ApiPullRequest {
)
case class Commit(sha: String, ref: String, repo: ApiRepository)(baseOwner: String) {
val label = if (baseOwner == repo.owner.login) { ref }
else { s"${repo.owner.login}:${ref}" }
val label = if (baseOwner == repo.owner.login) { ref } else { s"${repo.owner.login}:${ref}" }
val user = repo.owner
}

View File

@@ -40,12 +40,12 @@ object ApiRef {
ApiRef(
ref = s"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
// 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>"
// 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,
// which GH does for tags that are not annotated
//the GH api distinguishes between "releases" and plain git tags
//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>"
//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,
//which GH does for tags that are not annotated
`object` = ApiRefCommit(
sha = 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 clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s"/${full_name}.git"))
val ssh_url = Some(SshPath(""))
}
object ApiRepository {
@@ -61,7 +61,7 @@ object ApiRepository {
watchers = 0,
forks = 0,
`private` = false,
default_branch = "main",
default_branch = "master",
owner = owner,
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(
login = user.userName,
email = user.mailAddress,
`type` = if (user.isGroupAccount) { "Organization" }
else { "User" },
`type` = if (user.isGroupAccount) { "Organization" } else { "User" },
site_admin = user.isAdmin,
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 jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
(
{ case JString(s) =>
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)) }
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](
format =>
(
{
case JString(s) =>
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)) }
)
) + FieldSerializer[ApiUser]() +
FieldSerializer[ApiGroup]() +
@@ -44,35 +45,32 @@ object JsonFormat {
FieldSerializer[ApiCommits.File]() +
FieldSerializer[ApiRelease]() +
FieldSerializer[ApiReleaseAsset]() +
ApiBranchProtectionResponse.enforcementLevelSerializer
ApiBranchProtection.enforcementLevelSerializer
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) => throw new MappingException("Can't convert " + s + " to ApiPath")
},
{ case ApiPath(path) =>
JString(c.baseUrl + path)
}
)
}, {
case ApiPath(path) => JString(c.baseUrl + path)
})
)
def sshPathSerializer(c: Context) =
new CustomSerializer[SshPath](_ =>
(
{
new CustomSerializer[SshPath](
_ =>
({
case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) =>
SshPath(s.substring(c.sshUrl.get.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
},
{ case SshPath(path) =>
c.sshUrl.map { sshUrl =>
JString(sshUrl + path)
} getOrElse JNothing
}
)
}, {
case SshPath(path) =>
c.sshUrl.map { sshUrl =>
JString(sshUrl + path)
} getOrElse JNothing
})
)
/**

View File

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

View File

@@ -1,10 +1,10 @@
package gitbucket.core.controller
import gitbucket.core.api.*
import gitbucket.core.controller.api.*
import gitbucket.core.service.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import gitbucket.core.api._
import gitbucket.core.controller.api._
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.plugin.PluginRegistry
class ApiController
@@ -100,4 +100,16 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/gitbucket/plugins") {
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
import java.io.{File, FileInputStream, FileOutputStream}
import java.io.{File, FileInputStream}
import gitbucket.core.api.{ApiError, JsonFormat}
import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
import gitbucket.core.util.SyntaxSugars.*
import gitbucket.core.util.Directory.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import org.json4s.*
import org.scalatra.{MultiParams, *}
import org.scalatra.i18n.*
import org.scalatra.json.*
import org.scalatra.forms.*
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import org.json4s._
import org.scalatra._
import org.scalatra.i18n._
import org.scalatra.json._
import org.scalatra.forms._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
import is.tagomor.woothee.Classifier
import scala.util.Try
@@ -24,13 +25,10 @@ import net.coobird.thumbnailator.Thumbnails
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk.*
import org.eclipse.jgit.treewalk._
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import org.json4s.Formats
import org.json4s.jackson.Serialization
import java.nio.charset.StandardCharsets
/**
* Provides generic features for controller implementations.
@@ -48,21 +46,11 @@ abstract class ControllerBase
implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats
private case class HttpException(status: Int) extends RuntimeException
before("/api/v3/*") {
contentType = formats("json")
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 = {
val path = super.requestPath(uri, idx)
if (path != "/" && path.endsWith("/")) {
@@ -96,24 +84,17 @@ abstract class ControllerBase
*/
implicit def context: Context = {
contextCache.get match {
case null =>
case null => {
val context = Context(loadSystemSettings(), LoginAccount, request)
contextCache.set(context)
context
}
case context => context
}
}
private def LoginAccount: Option[Account] = {
request
.getAs[Account](Keys.Session.LoginAccount)
.orElse(session.getAs[Account](Keys.Session.LoginAccount))
.orElse {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
getLoginAccountFromLocalFile()
} else None
}
}
private def LoginAccount: Option[Account] =
request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
def ajaxGet(path: String)(action: => Any): Route =
super.get(path) {
@@ -139,7 +120,7 @@ abstract class ControllerBase
action(form)
}
protected def NotFound(): ActionResult =
protected def NotFound() =
if (request.hasAttribute(Keys.Request.Ajax)) {
org.scalatra.NotFound()
} 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)) {
org.scalatra.Unauthorized()
} else if (request.hasAttribute(Keys.Request.APIv3)) {
@@ -187,9 +168,7 @@ abstract class ControllerBase
}
error {
case e: HttpException =>
ActionResult(e.status, (), Map.empty)
case e =>
case e => {
logger.error(s"Catch unhandled error in request: ${request}", e)
if (request.hasAttribute(Keys.Request.Ajax)) {
org.scalatra.InternalServerError()
@@ -199,6 +178,7 @@ abstract class ControllerBase
} else {
org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e)))
}
}
}
override def url(
@@ -210,7 +190,7 @@ abstract class ControllerBase
withSessionId: Boolean = true
)(implicit request: HttpServletRequest, response: HttpServletResponse): String =
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.
@@ -254,9 +234,9 @@ abstract class ControllerBase
protected def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@scala.annotation.tailrec
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if walk.getPathString == path => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk)
case false => None
case true if (walk.getPathString == path) => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk)
case false => None
}
Using.resource(new TreeWalk(git.getRepository)) { treeWalk =>
@@ -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],
request: HttpServletRequest
) {
val path: String = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath: String = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl: String = settings.baseUrl(request)
val host: String = new java.net.URL(baseUrl).getHost
val platform: String = request.getHeader("User-Agent") match {
val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request)
val host = new java.net.URL(baseUrl).getHost
val platform = request.getHeader("User-Agent") match {
case null => null
case agent if agent.contains("Mac") => "mac"
case agent if agent.contains("Linux") => "linux"
case agent if agent.contains("Win") => "windows"
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 = {
loginAccount match {
@@ -433,16 +372,15 @@ trait AccountManagementControllerBase extends ControllerBase {
messages: Messages
): Option[String] = {
val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses"))
if (
extraMailAddresses.exists { case (k, v) =>
v.contains(value)
}
) {
if (extraMailAddresses.exists {
case (k, v) =>
v.contains(value)
}) {
Some("These mail addresses are duplicated.")
} else {
getAccountByMailAddress(value, includeRemoved = true)
getAccountByMailAddress(value, true)
.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."
}
}
@@ -457,23 +395,22 @@ trait AccountManagementControllerBase extends ControllerBase {
messages: Messages
): Option[String] = {
val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses"))
if (
params.optionValue("mailAddress").contains(value) || extraMailAddresses.count { case (k, v) =>
v.contains(value)
} > 1
) {
if (Some(value) == params.optionValue("mailAddress") || extraMailAddresses.count {
case (k, v) =>
v.contains(value)
} > 1) {
Some("These mail addresses are duplicated.")
} else {
getAccountByMailAddress(value, includeRemoved = true)
getAccountByMailAddress(value, true)
.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."
}
}
}
}
private val allReservedNames = Set(
val allReservedNames = Set(
"git",
"admin",
"upload",
@@ -492,9 +429,10 @@ trait AccountManagementControllerBase extends ControllerBase {
protected def reservedNames: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] =
if (allReservedNames.contains(value.toLowerCase)) {
Some(s"$value is reserved")
Some(s"${value} is reserved")
} else {
None
}
}
}

View File

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

View File

@@ -75,57 +75,59 @@ class FileUploadController
post("/wiki/:owner/:repository") {
setMultipartConfig()
// Don't accept not logged-in users
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account =>
val owner = params("owner")
val repository = params("repository")
session.get(Keys.Session.LoginAccount).collect {
case loginAccount: Account =>
val owner = params("owner")
val repository = params("repository")
// Check whether logged-in user is collaborator
onlyWikiEditable(owner, repository, loginAccount) {
execute(
{ (file, fileId) =>
val fileName = file.getName
LockUtil.lock(s"$owner/$repository/wiki") {
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
// Check whether logged-in user is collaborator
onlyWikiEditable(owner, repository, loginAccount) {
execute(
{ (file, fileId) =>
val fileName = file.getName
LockUtil.lock(s"${owner}/${repository}/wiki") {
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) {
git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
if (headId != null) {
JGitUtil.processTree(git, headId) { (path, tree) =>
if (path != fileName) {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
if (headId != null) {
JGitUtil.processTree(git, headId) { (path, tree) =>
if (path != fileName) {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
}
}
val bytes = IOUtils.toByteArray(file.getInputStream)
builder.add(
JGitUtil.createDirCacheEntry(
fileName,
FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, bytes)
)
)
builder.finish()
val newHeadId = JGitUtil.createNewCommit(
git,
inserter,
headId,
builder.getDirCache.writeTree(inserter),
Constants.HEAD,
loginAccount.fullName,
loginAccount.mailAddress,
s"Uploaded ${fileName}"
)
fileName
}
val bytes = IOUtils.toByteArray(file.getInputStream)
builder.add(
JGitUtil.createDirCacheEntry(
fileName,
FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, bytes)
)
)
builder.finish()
JGitUtil.createNewCommit(
git,
inserter,
headId,
builder.getDirCache.writeTree(inserter),
Constants.HEAD,
loginAccount.fullName,
loginAccount.mailAddress,
s"Uploaded $fileName"
)
fileName
}
}
},
_ => true
)
}
},
_ => true
)
}
} getOrElse BadRequest()
}
@@ -133,34 +135,32 @@ class FileUploadController
setMultipartConfigForLargeFile()
session
.get(Keys.Session.LoginAccount)
.collect { case _: Account =>
val owner = params("owner")
val repository = params("repository")
val tag = multiParams("splat").head
execute(
{ (file, fileId) =>
FileUtils.writeByteArrayToFile(
new File(getReleaseFilesDir(owner, repository), FileUtil.checkFilename(tag + "/" + fileId)),
file.get()
)
},
_ => true
)
.collect {
case _: Account =>
val owner = params("owner")
val repository = params("repository")
val tag = multiParams("splat").head
execute(
{ (file, fileId) =>
FileUtils.writeByteArrayToFile(
new File(getReleaseFilesDir(owner, repository), FileUtil.checkFilename(tag + "/" + fileId)),
file.get()
)
},
_ => true
)
}
.getOrElse(BadRequest())
}
post("/import") {
import JDBCUtil.*
import JDBCUtil._
setMultipartConfig()
session.get(Keys.Session.LoginAccount).collect {
case loginAccount: Account if loginAccount.isAdmin =>
execute(
{ (file, fileId) =>
request2Session(request).conn.importAsSQL(file.getInputStream)
},
_ => true
)
execute({ (file, fileId) =>
request2Session(request).conn.importAsSQL(file.getInputStream)
}, _ => true)
}
redirect("/admin/data")
}
@@ -168,13 +168,13 @@ class FileUploadController
private def setMultipartConfig(): Unit = {
val settings = loadSystemSettings()
val config = MultipartConfig(maxFileSize = Some(settings.upload.maxFileSize))
config.apply(request.getServletContext)
config.apply(request.getServletContext())
}
private def setMultipartConfigForLargeFile(): Unit = {
val settings = loadSystemSettings()
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 = {
@@ -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 {
case Some(file) if mimeTypeChecker(file.name) =>
val fileId = FileUtil.generateFileId

View File

@@ -1,20 +1,17 @@
package gitbucket.core.controller
import com.nimbusds.jwt.JWT
import java.net.URI
import com.nimbusds.oauth2.sdk.id.State
import com.nimbusds.openid.connect.sdk.Nonce
import gitbucket.core.helper.xml
import gitbucket.core.model.Account
import gitbucket.core.service.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import gitbucket.core.view.helpers.*
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view.helpers._
import org.scalatra.Ok
import org.scalatra.forms.*
import gitbucket.core.service.ActivityService.*
import org.scalatra.forms._
class IndexController
extends IndexControllerBase
@@ -34,12 +31,19 @@ class IndexController
with RequestCache
trait IndexControllerBase extends ControllerBase {
self: RepositoryService & ActivityService & AccountService & RepositorySearchService & UsersAuthenticator &
ReferrerAuthenticator & AccessTokenService & AccountFederationService & OpenIDConnectService =>
self: RepositoryService
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))),
"password" -> trim(label("Password", text(required))),
"hash" -> trim(optional(text()))
@@ -53,53 +57,38 @@ trait IndexControllerBase extends ControllerBase {
//
// case class SearchForm(query: String, owner: String, repository: String)
private case class OidcAuthContext(state: State, nonce: Nonce, redirectBackURI: String)
private case class OidcSessionContext(token: JWT)
case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String)
get("/") {
context.loginAccount
.map { account =>
// val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
if (!isNewsFeedEnabled) {
redirect("/dashboard/repos")
} else {
val repos = getVisibleRepositories(
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
gitbucket.core.html.index(
getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(
Some(account),
None,
withoutPhysicalInfo = true,
limit = false
limit = context.settings.basicBehavior.limitVisibleRepositories
),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName
)
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(
account.userName
),
enableNewsFeed = isNewsFeedEnabled
)
}
)
}
.getOrElse {
gitbucket.core.html.index(
activities = getRecentPublicActivities(),
recentRepositories = getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false,
enableNewsFeed = isNewsFeedEnabled
getRecentPublicActivities(),
getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false
)
}
}
get("/signin") {
if (context.loginAccount.nonEmpty) {
redirect("/")
}
params.get("redirect").foreach { redirect =>
if (redirect.startsWith("/")) {
flash.update(Keys.Flash.Redirect, redirect)
}
val redirect = params.get("redirect")
if (redirect.isDefined && redirect.get.startsWith("/")) {
flash.update(Keys.Flash.Redirect, redirect.get)
}
gitbucket.core.html.signin(flash.get("userName"), flash.get("password"), flash.get("error"))
}
@@ -131,8 +120,8 @@ trait IndexControllerBase extends ControllerBase {
case _ => "/"
}
session.setAttribute(
Keys.Session.OidcAuthContext,
OidcAuthContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
Keys.Session.OidcContext,
OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
)
redirect(authenticationRequest.toURI.toString)
} getOrElse {
@@ -146,10 +135,9 @@ trait IndexControllerBase extends ControllerBase {
get("/signin/oidc") {
context.settings.oidc.map { oidc =>
val redirectURI = new URI(s"$baseUrl/signin/oidc")
session.get(Keys.Session.OidcAuthContext) match {
case Some(context: OidcAuthContext) =>
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { case (jwt, account) =>
session.setAttribute(Keys.Session.OidcSessionContext, OidcSessionContext(jwt))
session.get(Keys.Session.OidcContext) match {
case Some(context: OidcContext) =>
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { account =>
signin(account, context.redirectBackURI)
} orElse {
flash.update("error", "Sorry, authentication failed. Please try again.")
@@ -167,18 +155,7 @@ trait IndexControllerBase extends ControllerBase {
}
get("/signout") {
context.settings.oidc.foreach { oidc =>
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()
}
session.invalidate
redirect("/")
}
@@ -196,24 +173,11 @@ trait IndexControllerBase extends ControllerBase {
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.
*/
private def signin(account: Account, redirectUrl: String = "/") = {
session.setAttribute(Keys.Session.LoginAccount, account)
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
saveLoginAccountToLocalFile(account)
}
updateLastLoginDate(account.userName)
if (LDAPUtil.isDummyMailAddress(account)) {
@@ -236,8 +200,8 @@ trait IndexControllerBase extends ControllerBase {
val group = params("group").toBoolean
org.json4s.jackson.Serialization.write(
Map(
"options" ->
getAllUsers(includeRemoved = false)
"options" -> (
getAllUsers(false)
.withFilter { t =>
(user, group) match {
case (true, true) => true
@@ -249,54 +213,43 @@ trait IndexControllerBase extends ControllerBase {
.map { t =>
Map(
"label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml(
StringUtil.cutTail(t.userName, 25, "...")
)}</b> ${StringUtil
.escapeHtml(StringUtil.cutTail(t.fullName, 25, "..."))}",
StringUtil.cutTail(t.userName, 25, "...")
)}</b> ${StringUtil
.escapeHtml(StringUtil.cutTail(t.fullName, 25, "..."))}",
"value" -> t.userName
)
}
)
)
)
})
/**
* JSON API for checking user or group existence.
*
* Returns a single string which is any of "group", "user" or "".
* Additionally, check whether the user is writable to the repository
* if "owner" and "repository" are given,
*/
post("/_user/existence")(usersOnly {
getAccountByUserNameIgnoreCase(params("userName")).map { account =>
if (!account.isGroupAccount && 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 ""
})
// TODO Move to RepositoryViewerController?
// TODO Move to RepositoryViewrController?
get("/:owner/:repository/search")(referrersOnly { repository =>
val query = params.getOrElse("q", "").trim
val target = params.getOrElse("type", "code")
val page =
try {
val i = params.getOrElse("page", "1").toInt
if (i <= 0) 1 else i
} catch {
case _: NumberFormatException => 1
}
val page = try {
val i = params.getOrElse("page", "1").toInt
if (i <= 0) 1 else i
} catch {
case _: NumberFormatException => 1
}
target.toLowerCase match {
case "issues" =>
gitbucket.core.search.html.issues(
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, pullRequest = false) else Nil,
pullRequest = false,
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, false) else Nil,
false,
query,
page,
repository
@@ -304,8 +257,8 @@ trait IndexControllerBase extends ControllerBase {
case "pulls" =>
gitbucket.core.search.html.issues(
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, pullRequest = true) else Nil,
pullRequest = true,
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, true) else Nil,
true,
query,
page,
repository
@@ -340,15 +293,15 @@ trait IndexControllerBase extends ControllerBase {
)
val repositories = {
if (context.settings.basicBehavior.limitVisibleRepositories) {
getVisibleRepositories(
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = false
)
} else {
visibleRepositories
context.settings.basicBehavior.limitVisibleRepositories match {
case true =>
getVisibleRepositories(
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = false
)
case false => visibleRepositories
}
}.filter { repository =>
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.model.{Account, CustomFieldBehavior}
import gitbucket.core.service.IssuesService.*
import gitbucket.core.service.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import gitbucket.core.service.IssuesService._
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import org.scalatra.forms.*
import org.scalatra.forms._
import org.scalatra.{BadRequest, Ok}
class IssuesController
@@ -34,12 +34,23 @@ class IssuesController
with RequestCache
trait IssuesControllerBase extends ControllerBase {
self: IssuesService & RepositoryService & AccountService & LabelsService & MilestonesService & ActivityService &
HandleCommentService & IssueCreationService & CustomFieldsService & ReadableUsersAuthenticator &
ReferrerAuthenticator & WritableUsersAuthenticator & PullRequestService & WebHookIssueCommentService &
PrioritiesService =>
self: IssuesService
with RepositoryService
with AccountService
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,
content: Option[String],
assigneeUserNames: Option[String],
@@ -47,10 +58,10 @@ trait IssuesControllerBase extends ControllerBase {
priorityId: Option[Int],
labelNames: Option[String]
)
private case class CommentForm(issueId: Int, content: String)
private case class IssueStateForm(issueId: Int, content: Option[String])
case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
private val issueCreateForm = mapping(
val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())),
"assigneeUserNames" -> trim(optional(text())),
@@ -59,63 +70,55 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply)
private val issueTitleEditForm = mapping(
val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required)))
)(x => x)
private val issueEditForm = mapping(
val issueEditForm = mapping(
"content" -> trim(optional(text()))
)(x => x)
private val commentForm = mapping(
val commentForm = mapping(
"issueId" -> label("Issue Id", number()),
"content" -> trim(label("Comment", text(required)))
)(CommentForm.apply)
private val issueStateForm = mapping(
val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { repository =>
val q = request.getParameter("q")
Option(q) match {
case Some(filter) if filter.contains("is:pr") =>
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 {
searchIssues(repository, condition, IssueSearchCondition.page(request))
}
case None =>
searchIssues(repository, IssueSearchCondition(request), IssueSearchCondition.page(request))
if (Option(q).exists(_.contains("is:pr"))) {
redirect(s"/${repository.owner}/${repository.name}/pulls?q=${StringUtil.urlEncode(q)}")
} else {
searchIssues(repository)
}
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
val issueId = params("id")
getIssue(repository.owner, repository.name, issueId) map { issue =>
if (issue.isPullRequest) {
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
} else {
html.issue(
issue,
getComments(repository.owner, repository.name, issueId.toInt),
getIssueLabels(repository.owner, repository.name, issueId.toInt),
getIssueAssignees(repository.owner, repository.name, issueId.toInt),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues),
isIssueEditable(repository),
isIssueManageable(repository),
isIssueCommentManageable(repository),
repository
)
}
getIssue(repository.owner, repository.name, issueId) map {
issue =>
if (issue.isPullRequest) {
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
html.issue(
issue,
getComments(repository.owner, repository.name, issueId.toInt),
getIssueLabels(repository.owner, repository.name, issueId.toInt),
getIssueAssignees(repository.owner, repository.name, issueId.toInt),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues),
isIssueEditable(repository),
isIssueManageable(repository),
isIssueCommentManageable(repository),
repository
)
}
} getOrElse NotFound()
})
@@ -136,120 +139,130 @@ trait IssuesControllerBase extends ControllerBase {
})
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator?
val issue = createIssue(
repository,
form.title,
form.content,
form.assigneeUserNames.toSeq.flatMap(_.split(",")),
form.milestoneId,
form.priorityId,
form.labelNames.toSeq.flatMap(_.split(",")),
loginAccount
)
context.withLoginAccount {
loginAccount =>
if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator?
val issue = createIssue(
repository,
form.title,
form.content,
form.assigneeUserNames.toSeq.flatMap(_.split(",")),
form.milestoneId,
form.priorityId,
form.labelNames.toSeq.flatMap(_.split(",")),
loginAccount
)
// Insert custom field values
params.toMap.foreach { case (key, value) =>
if (key.startsWith("custom-field-")) {
getCustomField(
repository.owner,
repository.name,
key.replaceFirst("^custom-field-", "").toInt
).foreach { field =>
CustomFieldBehavior.validate(field, value, messages) match {
case None =>
insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issue.issueId, value)
case Some(_) => halt(400)
// Insert custom field values
params.toMap.foreach {
case (key, value) =>
if (key.startsWith("custom-field-")) {
getCustomField(
repository.owner,
repository.name,
key.replaceFirst("^custom-field-", "").toInt
).foreach { field =>
CustomFieldBehavior.validate(field, value, messages) match {
case None =>
insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issue.issueId, value)
case Some(_) => halt(400)
}
}
}
}
}
}
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
} else Unauthorized()
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
} else Unauthorized()
}
})
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
context.withLoginAccount { loginAccount =>
getIssue(repository.owner, repository.name, params("id")).map { issue =>
if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
if (issue.title != title) {
// update issue
updateIssue(repository.owner, repository.name, issue.issueId, title, issue.content)
// extract references and create refer comment
createReferComment(repository.owner, repository.name, issue.copy(title = title), title, loginAccount)
createComment(
repository.owner,
repository.name,
loginAccount.userName,
issue.issueId,
issue.title + "\r\n" + title,
"change_title"
)
}
redirect(s"/${repository.owner}/${repository.name}/issues/_data/${issue.issueId}")
} else Unauthorized()
} getOrElse NotFound()
context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, params("id")).map {
issue =>
if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
if (issue.title != title) {
// update issue
updateIssue(repository.owner, repository.name, issue.issueId, title, issue.content)
// extract references and create refer comment
createReferComment(repository.owner, repository.name, issue.copy(title = title), title, loginAccount)
createComment(
repository.owner,
repository.name,
loginAccount.userName,
issue.issueId,
issue.title + "\r\n" + title,
"change_title"
)
}
redirect(s"/${repository.owner}/${repository.name}/issues/_data/${issue.issueId}")
} else Unauthorized()
} getOrElse NotFound()
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
context.withLoginAccount { loginAccount =>
getIssue(repository.owner, repository.name, params("id")).map { issue =>
if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
// update issue
updateIssue(repository.owner, repository.name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(repository.owner, repository.name, issue, content.getOrElse(""), loginAccount)
context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, params("id")).map { issue =>
if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
// update issue
updateIssue(repository.owner, repository.name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(repository.owner, repository.name, issue, content.getOrElse(""), loginAccount)
redirect(s"/${repository.owner}/${repository.name}/issues/_data/${issue.issueId}")
} else Unauthorized()
} getOrElse NotFound()
redirect(s"/${repository.owner}/${repository.name}/issues/_data/${issue.issueId}")
} else Unauthorized()
} getOrElse NotFound()
}
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt =
params
.get("action")
.filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-$id"
)
}
} getOrElse NotFound()
context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt =
params
.get("action")
.filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
handleComment(issue, Some(form.content), repository, actionOpt) map {
case (issue, id) =>
redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
)
}
} getOrElse NotFound()
}
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt =
params
.get("action")
.filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-$id"
)
}
} getOrElse NotFound()
context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt =
params
.get("action")
.filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
handleComment(issue, form.content, repository, actionOpt) map {
case (issue, id) =>
redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
)
}
} getOrElse NotFound()
}
})
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
getComment(repository.owner, repository.name, params("id")).map { comment =>
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content)
redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized()
} getOrElse NotFound()
context.withLoginAccount {
loginAccount =>
getComment(repository.owner, repository.name, params("id")).map { comment =>
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content)
redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized()
} getOrElse NotFound()
}
})
@@ -264,61 +277,65 @@ trait IssuesControllerBase extends ControllerBase {
})
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount =>
getIssue(repository.owner, repository.name, params("id")) map { x =>
if (isEditableContent(x.userName, x.repositoryName, x.openedUserName, loginAccount)) {
params.get("dataType") collect {
case t if t == "html" => html.editissue(x.content, x.issueId, repository)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"title" -> x.title,
"content" -> Markdown.toHtml(
markdown = x.content getOrElse "No description given.",
repository = repository,
branch = repository.repository.defaultBranch,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true
context.withLoginAccount {
loginAccount =>
getIssue(repository.owner, repository.name, params("id")) map {
x =>
if (isEditableContent(x.userName, x.repositoryName, x.openedUserName, loginAccount)) {
params.get("dataType") collect {
case t if t == "html" => html.editissue(x.content, x.issueId, repository)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"title" -> x.title,
"content" -> Markdown.toHtml(
markdown = x.content getOrElse "No description given.",
repository = repository,
branch = repository.repository.defaultBranch,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true
)
)
)
)
)
}
} else Unauthorized()
} getOrElse NotFound()
}
} else Unauthorized()
} getOrElse NotFound()
}
})
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount =>
getComment(repository.owner, repository.name, params("id")) map { x =>
if (isEditableContent(x.userName, x.repositoryName, x.commentedUserName, loginAccount)) {
params.get("dataType") collect {
case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"content" -> view.Markdown.toHtml(
markdown = x.content,
repository = repository,
branch = repository.repository.defaultBranch,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true
context.withLoginAccount {
loginAccount =>
getComment(repository.owner, repository.name, params("id")) map {
x =>
if (isEditableContent(x.userName, x.repositoryName, x.commentedUserName, loginAccount)) {
params.get("dataType") collect {
case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"content" -> view.Markdown.toHtml(
markdown = x.content,
repository = repository,
branch = repository.repository.defaultBranch,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true
)
)
)
)
)
}
} else Unauthorized()
} getOrElse NotFound()
}
} else Unauthorized()
} getOrElse NotFound()
}
})
@@ -330,48 +347,43 @@ trait IssuesControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository =>
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))
})
ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository =>
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))
})
ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
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()
})
ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
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()
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
updateMilestoneId(
repository.owner,
repository.name,
params("id").toInt,
milestoneId("milestoneId"),
insertComment = true
)
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"), true)
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId)
.map { case (_, openCount, closeCount) =>
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
.map {
case (_, openCount, closeCount) =>
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound()
} getOrElse Ok()
})
ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository =>
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")
})
@@ -433,7 +445,7 @@ trait IssuesControllerBase extends ControllerBase {
params("value").toIntOpt.map { labelId =>
executeBatch(repository) { issueId =>
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) {
redirect(params("uri"))
}
@@ -445,12 +457,12 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
val value = assignedUserName("value")
executeBatch(repository) {
// updateAssignedUserName(repository.owner, repository.name, _, value, true)
//updateAssignedUserName(repository.owner, repository.name, _, value, true)
value match {
case Some(assignedUserName) =>
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, insertComment = true)
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
case None =>
deleteAllIssueAssignees(repository.owner, repository.name, _, insertComment = true)
deleteAllIssueAssignees(repository.owner, repository.name, _, true)
}
}
if (params("uri").nonEmpty) {
@@ -461,20 +473,20 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository =>
val value = milestoneId("value")
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 =>
val value = priorityId("value")
executeBatch(repository) {
updatePriorityId(repository.owner, repository.name, _, value, insertComment = true)
updatePriorityId(repository.owner, repository.name, _, value, true)
}
})
get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
(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 =>
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
RawData(FileUtil.getSafeMimeType(file.getName), file)
@@ -490,26 +502,27 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"options" ->
"options" -> (
getOpenIssues(repository.owner, repository.name)
.map { t =>
Map(
"label" -> s"""${if (t.isPullRequest) "<i class='octicon octicon-git-pull-request'></i>"
else "<i class='octicon octicon-issue-opened'></i>"}<b> #${StringUtil
.escapeHtml(t.issueId.toString)} ${StringUtil
.escapeHtml(StringUtil.cutTail(t.title, 50, "..."))}</b>""",
else "<i class='octicon octicon-issue-opened'></i>"}<b> #${StringUtil
.escapeHtml(t.issueId.toString)} ${StringUtil
.escapeHtml(StringUtil.cutTail(t.title, 50, "..."))}</b>""",
"value" -> t.issueId.toString
)
}
)
)
)
})
private val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
private 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 assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: 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("from") match {
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
val issues =
searchIssue(
@@ -549,8 +565,8 @@ trait IssuesControllerBase extends ControllerBase {
/**
* 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
context: Context
private def isEditableContent(owner: String, repository: String, author: String, loginAccount: Account)(
implicit context: Context
): Boolean = {
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.
*/
private def isDeletableComment(owner: String, repository: String, author: String, loginAccount: Account)(implicit
context: Context
private def isDeletableComment(owner: String, repository: String, author: String, loginAccount: Account)(
implicit context: Context
): Boolean = {
hasOwnerRole(owner, repository, context.loginAccount) || author == loginAccount.userName
}

View File

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

View File

@@ -9,11 +9,11 @@ import gitbucket.core.service.{
MilestonesService,
RepositoryService
}
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.Implicits._
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 org.scalatra.forms.*
import org.scalatra.forms._
import org.scalatra.i18n.Messages
class MilestonesController
@@ -26,12 +26,15 @@ class MilestonesController
with WritableUsersAuthenticator
trait MilestonesControllerBase extends ControllerBase {
self: MilestonesService & RepositoryService & CommitStatusService & ReferrerAuthenticator &
WritableUsersAuthenticator =>
self: MilestonesService
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))),
"description" -> trim(label("Description", optional(text()))),
"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.
*/
get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) {
if (
!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
!context.currentPath.startsWith("/plugin-assets") && !context.currentPath.equals("/user.css") &&
!PluginRegistry().getAnonymousAccessiblePaths().exists { path =>
context.currentPath.startsWith(path)
}
) {
if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
!context.currentPath.startsWith("/plugin-assets") &&
!PluginRegistry().getAnonymousAccessiblePaths().exists { path =>
context.currentPath.startsWith(path)
}) {
Unauthorized()
} else {
pass()

View File

@@ -28,11 +28,15 @@ class PrioritiesController
with WritableUsersAuthenticator
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)))),
"description" -> trim(label("Description", optional(text(maxlength(255))))),
"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(
repository.owner,
repository.name,
@@ -100,7 +104,7 @@ trait PrioritiesControllerBase extends ControllerBase {
Ok()
})
ajaxPost("/:owner/:repository/issues/priorities/default")(writableUsersOnly { repository =>
ajaxPost("/:owner/:repository/issues/priorities/default")(writableUsersOnly { (repository) =>
setDefaultPriority(repository.owner, repository.name, priorityId("priorityId"))
Ok()
})
@@ -118,9 +122,9 @@ trait PrioritiesControllerBase extends ControllerBase {
private def priorityName: Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] =
if (value.contains(',')) {
Some(s"$name contains invalid character.")
Some(s"${name} contains invalid character.")
} else if (value.startsWith("_") || value.startsWith("-")) {
Some(s"$name starts with invalid character.")
Some(s"${name} starts with invalid character.")
} else {
None
}

View File

@@ -4,19 +4,17 @@ import gitbucket.core.model.activity.DeleteBranchInfo
import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
import gitbucket.core.service.IssuesService.*
import gitbucket.core.service.PullRequestService.*
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.*
import gitbucket.core.util.Directory.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import org.scalatra.forms.*
import gitbucket.core.service._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import org.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.scalatra.BadRequest
import java.nio.file.Files
import scala.util.Using
class PullRequestsController
@@ -42,19 +40,32 @@ class PullRequestsController
with RequestCache
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService & AccountService & IssuesService & MilestonesService & LabelsService & CustomFieldsService &
CommitsService & ActivityService & PullRequestService & WebHookPullRequestService & ReadableUsersAuthenticator &
ReferrerAuthenticator & WritableUsersAuthenticator & CommitStatusService & MergeService & ProtectedBranchService &
PrioritiesService =>
self: RepositoryService
with AccountService
with IssuesService
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))),
"content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(255))),
"targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> 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))),
"commitIdTo" -> trim(text(required, maxlength(40))),
"isDraft" -> trim(boolean(required)),
@@ -64,13 +75,13 @@ trait PullRequestsControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text()))
)(PullRequestForm.apply)
private val mergeForm = mapping(
val mergeForm = mapping(
"message" -> trim(label("Message", text(required))),
"strategy" -> trim(label("Strategy", text(required))),
"isDraft" -> trim(boolean(required))
)(MergeForm.apply)
private case class PullRequestForm(
case class PullRequestForm(
title: String,
content: Option[String],
targetUserName: String,
@@ -87,212 +98,207 @@ trait PullRequestsControllerBase extends ControllerBase {
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 =>
val q = request.getParameter("q")
Option(q) match {
case Some(filter) if filter.contains("is:issue") =>
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 {
searchPullRequests(repository, IssueSearchCondition(filter), IssueSearchCondition.page(request))
}
case None =>
searchPullRequests(repository, IssueSearchCondition(request), IssueSearchCondition.page(request))
if (Option(q).exists(_.contains("is:issue"))) {
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
} else {
searchPullRequests(None, repository)
}
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId =>
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(
repository.owner,
repository.name,
pullreq.commitIdFrom,
repository.owner,
repository.name,
pullreq.commitIdTo,
context.settings
)
params("id").toIntOpt.flatMap {
issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(
repository.owner,
repository.name,
pullreq.commitIdFrom,
repository.owner,
repository.name,
pullreq.commitIdTo
)
html.conversation(
issue,
pullreq,
commits.flatten,
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
diffs.size,
getIssueLabels(repository.owner, repository.name, issueId),
getIssueAssignees(repository.owner, repository.name, issueId),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId).filter(_._1.enableForPullRequests),
isEditable(repository),
isManageable(repository),
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
flash.iterator.map(f => f._1 -> f._2.toString).toMap
)
}
html.conversation(
issue,
pullreq,
commits.flatten,
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
diffs.size,
getIssueLabels(repository.owner, repository.name, issueId),
getIssueAssignees(repository.owner, repository.name, issueId),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId).filter(_._1.enableForPullRequests),
isEditable(repository),
isManageable(repository),
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
flash.iterator.map(f => f._1 -> f._2.toString).toMap
)
}
} getOrElse NotFound()
})
get("/:owner/:repository/pull/:id/commits")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId =>
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(
repository.owner,
repository.name,
pullreq.commitIdFrom,
repository.owner,
repository.name,
pullreq.commitIdTo,
context.settings
)
params("id").toIntOpt.flatMap {
issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(
repository.owner,
repository.name,
pullreq.commitIdFrom,
repository.owner,
repository.name,
pullreq.commitIdTo
)
val commitsWithStatus = commits.map { day =>
day.map { commit =>
(commit, getCommitStatusWithSummary(repository.owner, repository.name, commit.id))
}
val commitsWithStatus = commits.map { day =>
day.map { commit =>
(commit, getCommitStatusWithSummary(repository.owner, repository.name, commit.id))
}
}
html.commits(
issue,
pullreq,
commitsWithStatus,
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
diffs.size,
isManageable(repository),
repository
)
}
html.commits(
issue,
pullreq,
commitsWithStatus,
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
diffs.size,
isManageable(repository),
repository
)
}
} getOrElse NotFound()
})
get("/:owner/:repository/pull/:id/files")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId =>
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(
repository.owner,
repository.name,
pullreq.commitIdFrom,
repository.owner,
repository.name,
pullreq.commitIdTo,
context.settings
)
params("id").toIntOpt.flatMap {
issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(
repository.owner,
repository.name,
pullreq.commitIdFrom,
repository.owner,
repository.name,
pullreq.commitIdTo
)
html.files(
issue,
pullreq,
diffs,
commits.flatten,
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
isManageable(repository),
repository
)
}
html.files(
issue,
pullreq,
diffs,
commits.flatten,
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
isManageable(repository),
repository
)
}
} getOrElse NotFound()
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { issueId =>
getPullRequest(repository.owner, repository.name, issueId) map { case (issue, pullreq) =>
val conflictMessage = LockUtil.lock(s"${repository.owner}/${repository.name}") {
checkConflict(repository.owner, repository.name, pullreq.branch, issueId)
}
val hasMergePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(repository.owner, repository.name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
conflictMessage = conflictMessage,
commitStatuses = getCommitStatuses(repository.owner, repository.name, pullreq.commitIdTo),
branchProtection = branchProtection,
branchIsOutOfDate =
!JGitUtil.getShaByRef(repository.owner, repository.name, pullreq.branch).contains(pullreq.commitIdFrom),
needStatusCheck = context.loginAccount.forall { u =>
branchProtection.needStatusCheck(u.userName)
},
hasUpdatePermission = hasDeveloperRole(
pullreq.requestUserName,
pullreq.requestRepositoryName,
context.loginAccount
) &&
context.loginAccount.exists { u =>
!getProtectedBranchInfo(
params("id").toIntOpt.flatMap {
issueId =>
getPullRequest(repository.owner, repository.name, issueId) map {
case (issue, pullreq) =>
val conflictMessage = LockUtil.lock(s"${repository.owner}/${repository.name}") {
checkConflict(repository.owner, repository.name, pullreq.branch, issueId)
}
val hasMergePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(repository.owner, repository.name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
conflictMessage = conflictMessage,
commitStatuses = getCommitStatuses(repository.owner, repository.name, pullreq.commitIdTo),
branchProtection = branchProtection,
branchIsOutOfDate = JGitUtil.getShaByRef(repository.owner, repository.name, pullreq.branch) != Some(
pullreq.commitIdFrom
),
needStatusCheck = context.loginAccount.forall { u =>
branchProtection.needStatusCheck(u.userName)
},
hasUpdatePermission = hasDeveloperRole(
pullreq.requestUserName,
pullreq.requestRepositoryName,
pullreq.requestBranch
).needStatusCheck(u.userName)
},
hasMergePermission = hasMergePermission,
commitIdTo = pullreq.commitIdTo
)
html.mergeguide(
mergeStatus,
issue,
pullreq,
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get
)
}
context.loginAccount
) &&
context.loginAccount.exists { u =>
!getProtectedBranchInfo(
pullreq.requestUserName,
pullreq.requestRepositoryName,
pullreq.requestBranch
).needStatusCheck(u.userName)
},
hasMergePermission = hasMergePermission,
commitIdTo = pullreq.commitIdTo
)
html.mergeguide(
mergeStatus,
issue,
pullreq,
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get
)
}
} getOrElse NotFound()
})
get("/:owner/:repository/pull/:id/delete_branch")(readableUsersOnly { baseRepository =>
context.withLoginAccount { _ =>
(for {
issueId <- params("id").toIntOpt
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount)
} yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if (branchProtection.enabled) {
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
} else {
if (repository.repository.defaultBranch != pullreq.requestBranch) {
val userName = context.loginAccount.get.userName
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
val deleteBranchInfo =
DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
recordActivity(deleteBranchInfo)
}
createComment(
baseRepository.owner,
baseRepository.name,
userName,
issueId,
pullreq.requestBranch,
"delete_branch"
)
} else {
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
(for {
issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount)
} yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if (branchProtection.enabled) {
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
} else {
if (repository.repository.defaultBranch != pullreq.requestBranch) {
val userName = context.loginAccount.get.userName
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
recordActivity(deleteBranchInfo)
}
createComment(
baseRepository.owner,
baseRepository.name,
userName,
issueId,
pullreq.requestBranch,
"delete_branch"
)
} else {
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
}
}
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
}) getOrElse NotFound()
}
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
}) getOrElse NotFound()
})
post("/:owner/:repository/pull/:id/update_branch")(readableUsersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
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)
remoteRepository <- getRepository(pullreq.userName, pullreq.repositoryName)
owner = pullreq.requestUserName
@@ -303,36 +309,34 @@ trait PullRequestsControllerBase extends ControllerBase {
if (branchProtection.needStatusCheck(loginAccount.userName)) {
flash.update("error", s"branch ${pullreq.requestBranch} is protected need status check.")
} else {
LockUtil.lock(s"$owner/$name") {
LockUtil.lock(s"${owner}/${name}") {
val alias =
if (
pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName
) {
if (pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName) {
pullreq.branch
} else {
s"${pullreq.userName}:${pullreq.branch}"
}
// val existIds = Using
// .resource(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
// JGitUtil.getAllCommitIds(git)
// }
// .toSet
val existIds = Using
.resource(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
JGitUtil.getAllCommitIds(git)
}
.toSet
pullRemote(
repository,
pullreq.requestBranch,
remoteRepository,
pullreq.branch,
loginAccount,
s"Merge branch '$alias' into ${pullreq.requestBranch}",
s"Merge branch '${alias}' into ${pullreq.requestBranch}",
Some(pullreq),
context.settings
) match {
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) =>
// update pull request
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 =>
(for {
issueId <- params("id").toIntOpt
case (_, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
(_, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount)
@@ -365,11 +369,8 @@ trait PullRequestsControllerBase extends ControllerBase {
form.isDraft,
context.settings
) match {
case Right(result) =>
updateMergedCommitIds(repository.owner, repository.name, issueId, result.mergedCommitId)
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
case Left(message) =>
Some(BadRequest(message))
case Right(objectId) => redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(message) => Some(BadRequest(message))
}
} getOrElse NotFound()
}
@@ -379,27 +380,29 @@ trait PullRequestsControllerBase extends ControllerBase {
val headBranch = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) =>
getRepository(originUserName, originRepositoryName).map { originRepository =>
Using.resources(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
) { (oldGit, newGit) =>
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
val oldBranch = originRepository.branchList
.find(_ == newBranch)
.getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
getRepository(originUserName, originRepositoryName).map {
originRepository =>
Using.resources(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
) { (oldGit, newGit) =>
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
val oldBranch = originRepository.branchList
.find(_ == newBranch)
.getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/$originUserName:$oldBranch...$newBranch"
)
}
redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}"
)
}
} getOrElse NotFound()
case _ =>
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/$defaultBranch...${headBranch.getOrElse(defaultBranch)}"
)
JGitUtil.getDefaultBranch(git, forkedRepository).map {
case (_, defaultBranch) =>
redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}"
)
} getOrElse {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
}
@@ -420,7 +423,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner)
.map(_.repositoryName)
} else if (forkedRepository.repository.originUserName.contains(originOwner)) {
} else if (Some(originOwner) == forkedRepository.repository.originUserName) {
// Original repository
forkedRepository.repository.originRepositoryName
} else {
@@ -439,10 +442,8 @@ trait PullRequestsControllerBase extends ControllerBase {
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for (
originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
originRepository <- getRepository(originOwner, originRepositoryName)
) yield {
(for (originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
originRepository <- getRepository(originOwner, originRepositoryName)) yield {
val (oldId, newId) =
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
@@ -454,8 +455,7 @@ trait PullRequestsControllerBase extends ControllerBase {
oldId.getName,
forkedRepository.owner,
forkedRepository.name,
newId.getName,
context.settings
newId.getName
)
val title = if (commits.flatten.length == 1) {
@@ -481,9 +481,7 @@ trait PullRequestsControllerBase extends ControllerBase {
(repository.userName, repository.repositoryName, repository.defaultBranch)
},
commits.flatten
.flatMap(commit =>
getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, includePullRequest = false)
)
.flatMap(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false))
.toList,
originId,
forkedId,
@@ -504,8 +502,8 @@ trait PullRequestsControllerBase extends ControllerBase {
case (oldId, newId) =>
redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
s"$originOwner:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
s"$forkedOwner:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}"
s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}"
)
}
@@ -565,88 +563,87 @@ trait PullRequestsControllerBase extends ControllerBase {
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner)
(for {
originRepositoryName <-
if (originOwner == forkedOwner) {
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner)
.map(_.repositoryName)
}
}
originRepository <- getRepository(originOwner, originRepositoryName)
} yield {
(for (originRepositoryName <- if (originOwner == forkedOwner) {
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner)
.map(_.repositoryName)
}
};
originRepository <- getRepository(originOwner, originRepositoryName)) yield {
Using.resources(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
) { case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}") {
checkConflict(
originRepository.owner,
originRepository.name,
originBranch,
forkedRepository.owner,
forkedRepository.name,
forkedBranch
)
}
html.mergecheck(conflict.isDefined)
) {
case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}") {
checkConflict(
originRepository.owner,
originRepository.name,
originBranch,
forkedRepository.owner,
forkedRepository.name,
forkedBranch
)
}
html.mergecheck(conflict.isDefined)
}
}) getOrElse NotFound()
})
post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
val manageable = isManageable(repository)
context.withLoginAccount {
loginAccount =>
val manageable = isManageable(repository)
val issueId = insertIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginAccount.userName,
title = form.title,
content = form.content,
milestoneId = if (manageable) form.milestoneId else None,
priorityId = if (manageable) form.priorityId else None,
isPullRequest = true
)
val issueId = insertIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginAccount.userName,
title = form.title,
content = form.content,
milestoneId = if (manageable) form.milestoneId else None,
priorityId = if (manageable) form.priorityId else None,
isPullRequest = true
)
createPullRequest(
originRepository = repository,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo,
isDraft = form.isDraft,
loginAccount = loginAccount,
settings = context.settings
)
createPullRequest(
originRepository = repository,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo,
isDraft = form.isDraft,
loginAccount = loginAccount,
settings = context.settings
)
if (manageable) {
// insert assignees
form.assigneeUserNames.foreach { value =>
value.split(",").foreach { userName =>
registerIssueAssignee(repository.owner, repository.name, issueId, userName)
if (manageable) {
// insert assignees
form.assigneeUserNames.foreach { value =>
value.split(",").foreach { userName =>
registerIssueAssignee(repository.owner, repository.name, issueId, userName)
}
}
}
// insert labels
form.labelNames.foreach { value =>
val labels = getLabels(repository.owner, repository.name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
// insert labels
form.labelNames.foreach { value =>
val labels = getLabels(repository.owner, repository.name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
}
}
}
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
}
})
@@ -656,23 +653,24 @@ trait PullRequestsControllerBase extends ControllerBase {
context.loginAccount.map(x => Seq(x.mailAddress) ++ getAccountExtraMailAddresses(x.userName)).getOrElse(Nil)
val branches =
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
JGitUtil
.getBranches(
git = git,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
)
.filter { x =>
x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0 &&
x.commitTime.getTime > thresholdTime &&
mailAddresses.contains(x.committerEmailAddress)
}
.sortBy { br =>
(br.mergeInfo.isEmpty, br.commitTime)
}
.map(_.name)
.reverse
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
JGitUtil
.getBranches(
git = git,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
)
.filter { x =>
x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0 &&
x.commitTime.getTime > thresholdTime &&
mailAddresses.contains(x.committerEmailAddress)
}
.sortBy { br =>
(br.mergeInfo.isEmpty, br.commitTime)
}
.map(_.name)
.reverse
}
val targetRepository = (for {
@@ -692,11 +690,10 @@ trait PullRequestsControllerBase extends ControllerBase {
html.proposals(proposedBranches, targetRepository, repository)
})
private def searchPullRequests(
repository: RepositoryService.RepositoryInfo,
condition: IssueSearchCondition,
page: Int
) = {
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = {
val page = IssueSearchCondition.page(request)
// retrieve search condition
val condition = IssueSearchCondition(request)
// search issues
val issues = searchIssue(
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.
*/
@@ -848,4 +744,5 @@ trait PullRequestsControllerBase extends ControllerBase {
case "DISABLE" => false
}
}
}

View File

@@ -11,10 +11,10 @@ import gitbucket.core.service.{
RepositoryService,
RequestCache
}
import gitbucket.core.util.*
import gitbucket.core.util.Directory.*
import gitbucket.core.util.Implicits.*
import org.scalatra.forms.*
import gitbucket.core.util._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import org.scalatra.forms._
import gitbucket.core.releases.html
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
@@ -33,15 +33,20 @@ class ReleaseController
with RequestCache
trait ReleaseControllerBase extends ControllerBase {
self: RepositoryService & AccountService & ReleaseService & ReadableUsersAuthenticator & ReferrerAuthenticator &
WritableUsersAuthenticator & ActivityService =>
self: RepositoryService
with AccountService
with ReleaseService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with ActivityService =>
private case class ReleaseForm(
case class ReleaseForm(
name: String,
content: Option[String]
)
private val releaseForm = mapping(
val releaseForm = mapping(
"name" -> trim(text(required)),
"content" -> trim(optional(text()))
)(ReleaseForm.apply)
@@ -101,39 +106,42 @@ trait ReleaseControllerBase extends ControllerBase {
})
post("/:owner/:repository/releases/*/create", releaseForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
val tagName = multiParams("splat").head
context.withLoginAccount {
loginAccount =>
val tagName = multiParams("splat").head
// Insert into RELEASE
createRelease(repository.owner, repository.name, form.name, form.content, tagName, loginAccount)
// Insert into RELEASE
createRelease(repository.owner, repository.name, form.name, form.content, tagName, loginAccount)
// Insert into RELEASE_ASSET
val files = params.toMap.collect {
case (name, value) if name.startsWith("file:") =>
val Array(_, fileId) = name.split(":")
(fileId, value)
}
files.foreach { case (fileId, fileName) =>
val size =
new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(tagName + "/" + fileId)
).length
createReleaseAsset(repository.owner, repository.name, tagName, fileId, fileName, size, loginAccount)
}
// Insert into RELEASE_ASSET
val files = params.toMap.collect {
case (name, value) if name.startsWith("file:") =>
val Array(_, fileId) = name.split(":")
(fileId, value)
}
files.foreach {
case (fileId, fileName) =>
val size =
new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(tagName + "/" + fileId)
).length
createReleaseAsset(repository.owner, repository.name, tagName, fileId, fileName, size, loginAccount)
}
val releaseInfo = ReleaseInfo(repository.owner, repository.name, loginAccount.userName, form.name, tagName)
recordActivity(releaseInfo)
val releaseInfo = ReleaseInfo(repository.owner, repository.name, loginAccount.userName, form.name, tagName)
recordActivity(releaseInfo)
redirect(s"/${repository.owner}/${repository.name}/releases/$tagName")
redirect(s"/${repository.owner}/${repository.name}/releases/${tagName}")
}
})
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
.map { commit =>
s"- ${commit.shortMessage} ${commit.id}"
@@ -163,45 +171,48 @@ trait ReleaseControllerBase extends ControllerBase {
})
post("/:owner/:repository/releases/*/edit", releaseForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
val tagName = multiParams("splat").head
context.withLoginAccount {
loginAccount =>
val tagName = multiParams("splat").head
getRelease(repository.owner, repository.name, tagName)
.map { release =>
// Update RELEASE
updateRelease(repository.owner, repository.name, tagName, form.name, form.content)
getRelease(repository.owner, repository.name, tagName)
.map {
release =>
// Update RELEASE
updateRelease(repository.owner, repository.name, tagName, form.name, form.content)
// Delete and Insert RELEASE_ASSET
val assets = getReleaseAssets(repository.owner, repository.name, tagName)
deleteReleaseAssets(repository.owner, repository.name, tagName)
// Delete and Insert RELEASE_ASSET
val assets = getReleaseAssets(repository.owner, repository.name, tagName)
deleteReleaseAssets(repository.owner, repository.name, tagName)
val files = params.toMap.collect {
case (name, value) if name.startsWith("file:") =>
val Array(_, fileId) = name.split(":")
(fileId, value)
val files = params.toMap.collect {
case (name, value) if name.startsWith("file:") =>
val Array(_, fileId) = name.split(":")
(fileId, value)
}
files.foreach {
case (fileId, fileName) =>
val size =
new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(tagName + "/" + fileId)
).length
createReleaseAsset(repository.owner, repository.name, tagName, fileId, fileName, size, loginAccount)
}
assets.foreach { asset =>
if (!files.exists { case (fileId, _) => fileId == asset.fileName }) {
val file = new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(release.tag + "/" + asset.fileName)
)
FileUtils.forceDelete(file)
}
}
redirect(s"/${release.userName}/${release.repositoryName}/releases/${tagName}")
}
files.foreach { case (fileId, fileName) =>
val size =
new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(tagName + "/" + fileId)
).length
createReleaseAsset(repository.owner, repository.name, tagName, fileId, fileName, size, loginAccount)
}
assets.foreach { asset =>
if (!files.exists { case (fileId, _) => fileId == asset.fileName }) {
val file = new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(release.tag + "/" + asset.fileName)
)
FileUtils.forceDelete(file)
}
}
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) = {
import gitbucket.core.service.ReleaseService.*
import gitbucket.core.service.ReleaseService._
val (offset, limit) = ((page - 1) * ReleaseLimit, ReleaseLimit)
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 tagsWithReleases = tagsToDisplay.map { tag =>
(
tag,
releases.find(_.tag == tag.name).map { release =>
(release, assets(release))
}
)
(tag, releases.find(_.tag == tag.name).map { release =>
(release, assets(release))
})
}
tagsWithReleases
}

View File

@@ -4,16 +4,16 @@ import java.time.{LocalDateTime, ZoneOffset}
import java.util.Date
import gitbucket.core.settings.html
import gitbucket.core.model.{RepositoryWebHook, WebHook}
import gitbucket.core.service.*
import gitbucket.core.service.WebHookService.*
import gitbucket.core.util.*
import gitbucket.core.util.JGitUtil.*
import gitbucket.core.util.SyntaxSugars.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.Directory.*
import gitbucket.core.service._
import gitbucket.core.service.WebHookService._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.WebHookContentType
import gitbucket.core.model.activity.RenameRepositoryInfo
import org.scalatra.forms.*
import org.scalatra.forms._
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
@@ -37,11 +37,19 @@ class RepositorySettingsController
with RequestCache
trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService & AccountService & WebHookService & ProtectedBranchService & CommitStatusService &
DeployKeyService & CustomFieldsService & ActivityService & OwnerAuthenticator & UsersAuthenticator =>
self: RepositoryService
with AccountService
with WebHookService
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with CustomFieldsService
with ActivityService
with OwnerAuthenticator
with UsersAuthenticator =>
// for repository options
private case class OptionsForm(
case class OptionsForm(
description: Option[String],
isPrivate: Boolean,
issuesOption: String,
@@ -54,7 +62,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
safeMode: Boolean
)
private val optionsForm = mapping(
val optionsForm = mapping(
"description" -> trim(label("Description", optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"issuesOption" -> trim(label("Issues Option", text(required, featureOption))),
@@ -72,30 +80,25 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
// 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))))
)(DefaultBranchForm.apply)
// 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)))),
"publicKey" -> trim2(label("Key", text(required))), // TODO duplication check in the repository?
"allowWrite" -> trim(label("Key", boolean()))
)(DeployKeyForm.apply)
// for web hook url addition
private case class WebHookForm(
url: String,
events: Set[WebHook.Event],
ctype: WebHookContentType,
token: Option[String]
)
case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
private def webHookForm(update: Boolean) =
def webHookForm(update: Boolean) =
mapping(
"url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents,
@@ -104,34 +107,32 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)((url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token))
// for rename repository
private case class RenameRepositoryForm(repositoryName: String)
case class RenameRepositoryForm(repositoryName: String)
private val renameForm = mapping(
val renameForm = mapping(
"repositoryName" -> trim(
label("New repository name", text(required, maxlength(100), repository, renameRepositoryName))
)
)(RenameRepositoryForm.apply)
// 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)))
)(TransferOwnerShipForm.apply)
// for custom field
private case class CustomFieldForm(
case class CustomFieldForm(
fieldName: String,
fieldType: String,
constraints: Option[String],
enableForIssues: Boolean,
enableForPullRequests: Boolean
)
private val customFieldForm = mapping(
val customFieldForm = mapping(
"fieldName" -> trim(label("Field name", text(required, maxlength(100)))),
"fieldType" -> trim(label("Field type", text(required))),
"constraints" -> trim(label("Constraints", optional(text()))),
"enableForIssues" -> trim(label("Enable for issues", boolean(required))),
"enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))),
)(CustomFieldForm.apply)
@@ -183,7 +184,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
if (!repository.branchList.contains(form.defaultBranch)) {
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
redirect(s"/${repository.owner}/${repository.name}/settings/options")
} else {
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
// Change repository HEAD
@@ -197,13 +198,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** Branch protection for branch */
get("/:owner/:repository/settings/branches/*")(ownerOnly { repository =>
import gitbucket.core.api.*
import gitbucket.core.api._
val branch = params("splat")
if (!repository.branchList.contains(branch)) {
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else {
val protection = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatusContexts(
repository.owner,
repository.name,
@@ -253,7 +254,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
ctype = WebHookContentType.FORM,
token = None
)
html.edithook(webhook, Set(WebHook.Push), repository, create = true)
html.edithook(webhook, Set(WebHook.Push), repository, true)
})
/**
@@ -283,90 +284,93 @@ trait RepositorySettingsControllerBase extends ControllerBase {
Array(h.getName, h.getValue)
}
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
import scala.concurrent.duration.*
import scala.concurrent.*
import scala.jdk.CollectionConverters.*
import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
import scala.concurrent.duration._
import scala.concurrent._
import scala.jdk.CollectionConverters._
import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global
val url = params("url")
val token = Some(params("token"))
val ctype = WebHookContentType.valueOf(params("ctype"))
val dummyWebHookInfo = RepositoryWebHook(
userName = repository.owner,
repositoryName = repository.name,
url = url,
ctype = ctype,
token = token
)
val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get
val commits =
if (JGitUtil.isEmpty(git)) List.empty
else
git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(4)
.call
.iterator
.asScala
.map(new CommitInfo(_))
.toList
val pushedCommit = commits.drop(1)
WebHookPushPayload(
git = git,
sender = ownerAccount,
refName = "refs/heads/" + repository.repository.defaultBranch,
repositoryInfo = repository,
commits = pushedCommit,
repositoryOwner = ownerAccount,
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId())
val url = params("url")
val token = Some(params("token"))
val ctype = WebHookContentType.valueOf(params("ctype"))
val dummyWebHookInfo = RepositoryWebHook(
userName = repository.owner,
repositoryName = repository.name,
url = url,
ctype = ctype,
token = token
)
}
val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get
val commits =
if (JGitUtil.isEmpty(git)) List.empty
else
git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(4)
.call
.iterator
.asScala
.map(new CommitInfo(_))
.toList
val pushedCommit = commits.drop(1)
val (webHook, json, reqFuture, resFuture) =
callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head
WebHookPushPayload(
git = git,
sender = ownerAccount,
refName = "refs/heads/" + repository.repository.defaultBranch,
repositoryInfo = repository,
commits = pushedCommit,
repositoryOwner = ownerAccount,
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId())
)
}
val toErrorMap: PartialFunction[Throwable, Map[String, String]] = {
case e: java.net.UnknownHostException => Map("error" -> s"Unknown host ${e.getMessage}")
case _: java.lang.IllegalArgumentException => Map("error" -> "invalid url")
case _: org.apache.http.client.ClientProtocolException => Map("error" -> "invalid url")
case NonFatal(e) => Map("error" -> s"${e.getClass} ${e.getMessage}")
}
val (webHook, json, reqFuture, resFuture) =
callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"url" -> url,
"request" -> Await.result(
reqFuture
.map(req =>
Map(
"headers" -> _headers(req.getAllHeaders),
"payload" -> json
val toErrorMap: PartialFunction[Throwable, Map[String, String]] = {
case e: java.net.UnknownHostException => Map("error" -> ("Unknown host " + e.getMessage))
case e: java.lang.IllegalArgumentException => 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}"))
}
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"url" -> url,
"request" -> Await.result(
reqFuture
.map(
req =>
Map(
"headers" -> _headers(req.getAllHeaders),
"payload" -> json
)
)
)
.recover(toErrorMap),
20 seconds
),
"response" -> Await.result(
resFuture
.map(res =>
Map(
"status" -> res.getStatusLine.getStatusCode,
"body" -> EntityUtils.toString(res.getEntity),
"headers" -> _headers(res.getAllHeaders)
.recover(toErrorMap),
20 seconds
),
"response" -> Await.result(
resFuture
.map(
res =>
Map(
"status" -> res.getStatusLine.getStatusCode,
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
)
)
)
.recover(toErrorMap),
20 seconds
.recover(toErrorMap),
20 seconds
)
)
)
)
}
})
@@ -374,8 +378,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook edit page.
*/
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map { case (webhook, events) =>
html.edithook(webhook, events, repository, create = false)
getWebHook(repository.owner, repository.name, params("url")).map {
case (webhook, events) =>
html.edithook(webhook, events, repository, false)
} getOrElse NotFound()
})
@@ -399,22 +404,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Rename repository.
*/
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
if (repository.name != form.repositoryName) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
// Record activity log
val renameInfo = RenameRepositoryInfo(
repository.owner,
form.repositoryName,
loginAccount.userName,
repository.name
)
recordActivity(renameInfo)
}
redirect(s"/${repository.owner}/${form.repositoryName}")
} else Forbidden()
context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
if (repository.name != form.repositoryName) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
// Record activity log
val renameInfo = RenameRepositoryInfo(
repository.owner,
form.repositoryName,
loginAccount.userName,
repository.name
)
recordActivity(renameInfo)
}
redirect(s"/${repository.owner}/${form.repositoryName}")
} else Forbidden()
}
})
@@ -422,23 +428,24 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Transfer repository ownership.
*/
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) {
// Change repository owner
if (repository.owner != form.newOwner) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Record activity log
val renameInfo = RenameRepositoryInfo(
form.newOwner,
repository.name,
loginAccount.userName,
repository.owner
)
recordActivity(renameInfo)
}
redirect(s"/${form.newOwner}/${repository.name}")
} else Forbidden()
context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) {
// Change repository owner
if (repository.owner != form.newOwner) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Record activity log
val renameInfo = RenameRepositoryInfo(
form.newOwner,
repository.name,
loginAccount.userName,
repository.owner
)
recordActivity(renameInfo)
}
redirect(s"/${form.newOwner}/${repository.name}")
} else Forbidden()
}
})
@@ -504,7 +511,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository.name,
form.fieldName,
form.fieldType,
if (form.fieldType == "enum") form.constraints else None,
form.enableForIssues,
form.enableForPullRequests
)
@@ -527,7 +533,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
params("fieldId").toInt,
form.fieldName,
form.fieldType,
if (form.fieldType == "enum") form.constraints else None,
form.enableForIssues,
form.enableForPullRequests
)
@@ -628,7 +633,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case None => Some("User does not exist.")
case Some(x) =>
if (x.userName == params("owner")) {
Some("This is current repository owner.")

View File

@@ -3,17 +3,17 @@ package gitbucket.core.controller
import java.io.FileInputStream
import gitbucket.core.admin.html
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.*
import gitbucket.core.service.SystemSettingsService._
import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.ssh.SshServer
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.StringUtil.*
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.{AdminAuthenticator, Mailer}
import org.apache.commons.io.IOUtils
import org.apache.commons.mail.EmailException
import org.json4s.jackson.Serialization
import org.scalatra.*
import org.scalatra.forms.*
import org.scalatra._
import org.scalatra.forms._
import org.scalatra.i18n.Messages
import scala.collection.mutable.ListBuffer
@@ -29,7 +29,7 @@ case class Table(name: String, columns: Seq[Column])
case class Column(name: String, primaryKey: Boolean)
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
self: AccountService & RepositoryService & AdminAuthenticator =>
self: AccountService with RepositoryService with AdminAuthenticator =>
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
@@ -55,14 +55,16 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"bindAddress" -> mapping(
"host" -> trim(label("Bind SSH host", optional(text()))),
"port" -> trim(label("Bind SSH port", optional(number()))),
)((hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
)(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
),
"publicAddress" -> mapping(
"host" -> trim(label("Public SSH host", optional(text()))),
"port" -> trim(label("Public SSH port", optional(number()))),
)((hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
)(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
),
)(Ssh.apply),
"useSMTP" -> trim(label("SMTP", boolean())),
@@ -121,11 +123,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"largeTimeout" -> trim(label("Timeout for large file", long(required)))
)(Upload.apply),
"repositoryViewer" -> mapping(
"maxFiles" -> trim(label("Max files", number(required))),
"maxDiffFiles" -> trim(label("Max diff files", number(required))),
"maxDiffLines" -> trim(label("Max diff lines", number(required)))
)(RepositoryViewerSettings.apply),
"defaultBranch" -> trim(label("Default branch", text(required)))
"maxFiles" -> trim(label("Max files", number(required)))
)(RepositoryViewerSettings.apply)
)(SystemSettings.apply).verifying { settings =>
Vector(
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
@@ -151,11 +150,11 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"testAddress" -> trim(label("", text(required)))
)(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,
password: String,
fullName: String,
@@ -167,7 +166,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
fileId: Option[String]
)
private case class EditUserForm(
case class EditUserForm(
userName: String,
password: Option[String],
fullName: String,
@@ -181,7 +180,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
isRemoved: Boolean
)
private case class NewGroupForm(
case class NewGroupForm(
groupName: String,
description: Option[String],
url: Option[String],
@@ -189,7 +188,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
members: String
)
private case class EditGroupForm(
case class EditGroupForm(
groupName: String,
description: Option[String],
url: Option[String],
@@ -199,7 +198,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
isRemoved: Boolean
)
private val newUserForm = mapping(
val newUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password", text(required, maxlength(40)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
@@ -213,7 +212,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"fileId" -> trim(label("File ID", optional(text())))
)(NewUserForm.apply)
private val editUserForm = mapping(
val editUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier))),
"password" -> trim(label("Password", optional(text(maxlength(40))))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))),
@@ -229,7 +228,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"removed" -> trim(label("Disable", boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply)
private val newGroupForm = mapping(
val newGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))),
@@ -237,7 +236,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"members" -> trim(label("Members", text(required, members)))
)(NewGroupForm.apply)
private val editGroupForm = mapping(
val editGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier))),
"description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))),
@@ -251,27 +250,28 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val conn = request2Session(request).conn
val meta = conn.getMetaData
val tables = ListBuffer[Table]()
Using.resource(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))) { rs =>
while (rs.next()) {
val tableName = rs.getString("TABLE_NAME")
Using.resource(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))) {
rs =>
while (rs.next()) {
val tableName = rs.getString("TABLE_NAME")
val pkColumns = ListBuffer[String]()
Using.resource(meta.getPrimaryKeys(null, null, tableName)) { rs =>
while (rs.next()) {
pkColumns += rs.getString("COLUMN_NAME").toUpperCase
val pkColumns = ListBuffer[String]()
Using.resource(meta.getPrimaryKeys(null, null, tableName)) { rs =>
while (rs.next()) {
pkColumns += rs.getString("COLUMN_NAME").toUpperCase
}
}
}
val columns = ListBuffer[Column]()
Using.resource(meta.getColumns(null, "%", tableName, "%")) { rs =>
while (rs.next()) {
val columnName = rs.getString("COLUMN_NAME").toUpperCase
columns += Column(columnName, pkColumns.contains(columnName))
val columns = ListBuffer[Column]()
Using.resource(meta.getColumns(null, "%", tableName, "%")) { rs =>
while (rs.next()) {
val columnName = rs.getString("COLUMN_NAME").toUpperCase
columns += Column(columnName, pkColumns.contains(columnName))
}
}
}
tables += Table(tableName.toUpperCase, columns.toSeq)
}
tables += Table(tableName.toUpperCase, columns.toSeq)
}
}
html.dbviewer(tables.toSeq)
})
@@ -284,26 +284,28 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
if (trimmedQuery.nonEmpty) {
try {
val conn = request2Session(request).conn
Using.resource(conn.prepareStatement(query)) { stmt =>
if (trimmedQuery.toUpperCase.startsWith("SELECT")) {
Using.resource(stmt.executeQuery()) { rs =>
val meta = rs.getMetaData
val columns = for (i <- 1 to meta.getColumnCount) yield {
meta.getColumnName(i)
Using.resource(conn.prepareStatement(query)) {
stmt =>
if (trimmedQuery.toUpperCase.startsWith("SELECT")) {
Using.resource(stmt.executeQuery()) {
rs =>
val meta = rs.getMetaData
val columns = for (i <- 1 to meta.getColumnCount) yield {
meta.getColumnName(i)
}
val result = ListBuffer[Map[String, String]]()
while (rs.next()) {
val row = columns.map { columnName =>
columnName -> Option(rs.getObject(columnName)).map(_.toString).getOrElse("<NULL>")
}.toMap
result += row
}
Ok(Serialization.write(Map("type" -> "query", "columns" -> columns, "rows" -> result)))
}
val result = ListBuffer[Map[String, String]]()
while (rs.next()) {
val row = columns.map { columnName =>
columnName -> Option(rs.getObject(columnName)).map(_.toString).getOrElse("<NULL>")
}.toMap
result += row
}
Ok(Serialization.write(Map("type" -> "query", "columns" -> columns, "rows" -> result)))
} else {
val rows = stmt.executeUpdate()
Ok(Serialization.write(Map("type" -> "update", "rows" -> rows)))
}
} else {
val rows = stmt.executeUpdate()
Ok(Serialization.write(Map("type" -> "update", "rows" -> rows)))
}
}
} catch {
case e: Exception =>
@@ -320,9 +322,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if (
form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress
) {
if (form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress) {
SshServer.stop()
for {
bindAddress <- form.ssh.bindAddress
@@ -363,7 +363,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
})
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.")
redirect("/admin/plugins")
})
@@ -385,7 +385,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val includeGroups = params.get("includeGroups").exists(_.toBoolean)
val users = getAllUsers(includeRemoved, includeGroups)
val members = users.collect {
case account if account.isGroupAccount =>
case account if (account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
@@ -406,7 +406,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
form.description,
form.url
)
updateImage(form.userName, form.fileId, clearImage = false)
updateImage(form.userName, form.fileId, false)
updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != ""))
redirect("/admin/users")
})
@@ -414,48 +414,49 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("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 =>
val userName = params("userName")
getAccountByUserName(userName, includeRemoved = true).map { 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.")
redirect(s"/admin/users/${userName}/_edituser")
} else {
if (form.isRemoved) {
// Remove repositories
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER and COLLABORATOR
removeUserRelatedData(userName)
}
getAccountByUserName(userName, true).map {
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.")
redirect(s"/admin/users/${userName}/_edituser")
} else {
if (form.isRemoved) {
// Remove repositories
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER and COLLABORATOR
removeUserRelatedData(userName)
}
updateAccount(
account.copy(
password = form.password.map(pbkdf2_sha256).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
description = form.description,
url = form.url,
isRemoved = form.isRemoved
updateAccount(
account.copy(
password = form.password.map(pbkdf2_sha256).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
description = form.description,
url = form.url,
isRemoved = form.isRemoved
)
)
)
updateImage(userName, form.fileId, form.clearImage)
updateAccountExtraMailAddresses(userName, form.extraMailAddresses.filter(_ != ""))
updateImage(userName, form.fileId, form.clearImage)
updateAccountExtraMailAddresses(userName, form.extraMailAddresses.filter(_ != ""))
// call hooks
if (form.isRemoved) PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
// call hooks
if (form.isRemoved) PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
redirect("/admin/users")
}
redirect("/admin/users")
}
} getOrElse NotFound()
})
@@ -476,13 +477,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
}
.toList
)
updateImage(form.groupName, form.fileId, clearImage = false)
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
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 =>
@@ -496,12 +497,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
}
.toList
getAccountByUserName(groupName, includeRemoved = true).map { account =>
updateGroup(groupName, form.description, form.url, form.isRemoved)
getAccountByUserName(groupName, true).map {
account =>
updateGroup(groupName, form.description, form.url, form.isRemoved)
if (form.isRemoved) {
// Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil)
if (form.isRemoved) {
// Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil)
// // Remove repositories
// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
// deleteRepository(groupName, repositoryName)
@@ -509,9 +511,9 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
// FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
// }
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// // Update COLLABORATOR for group repositories
// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
// removeCollaborators(form.groupName, repositoryName)
@@ -519,22 +521,22 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
// addCollaborator(form.groupName, repositoryName, userName)
// }
// }
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound()
})
get("/admin/data")(adminOnly {
import gitbucket.core.util.JDBCUtil.*
import gitbucket.core.util.JDBCUtil._
val session = request2Session(request)
html.data(session.conn.allTableNames())
})
post("/admin/export")(adminOnly {
import gitbucket.core.util.JDBCUtil.*
import gitbucket.core.util.JDBCUtil._
val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
contentType = "application/octet-stream"
@@ -549,7 +551,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
})
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] = {
if (value == null) {
Nil
@@ -562,11 +564,9 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
private def members: Constraint =
new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if (
value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}
) None
if (value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None
else Some("Must select one manager at least.")
}
}
@@ -577,7 +577,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
for {
userName <- params.get(paramName)
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"
}
}

View File

@@ -1,16 +1,16 @@
package gitbucket.core.controller
import org.json4s.{JField, JObject, JString}
import org.scalatra.*
import org.scalatra.json.*
import org.scalatra.forms.*
import org.scalatra._
import org.scalatra.json._
import org.scalatra.forms._
import org.scalatra.i18n.I18nSupport
import org.scalatra.servlet.ServletBase
/**
* 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 = {
registerValidate(path, form)
@@ -84,8 +84,9 @@ trait ValidationSupport extends FormSupport { self: ServletBase & JacksonJsonSup
* Converts errors to JSON.
*/
private def toJson(errors: Seq[(String, String)]): JObject =
JObject(errors.map { case (key, value) =>
JField(key, JString(value))
JObject(errors.map {
case (key, value) =>
JField(key, JString(value))
}.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.WebHookService.WebHookGollumPayload
import gitbucket.core.wiki.html
import gitbucket.core.service.*
import gitbucket.core.util.*
import gitbucket.core.util.StringUtil.*
import gitbucket.core.util.SyntaxSugars.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.Directory.*
import org.scalatra.forms.*
import gitbucket.core.service._
import gitbucket.core.util._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import org.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages
@@ -29,10 +29,15 @@ class WikiController
with RequestCache
trait WikiControllerBase extends ControllerBase {
self: WikiService & RepositoryService & AccountService & ActivityService & WebHookService &
ReadableUsersAuthenticator & ReferrerAuthenticator =>
self: WikiService
with RepositoryService
with AccountService
with ActivityService
with WebHookService
with ReadableUsersAuthenticator
with ReferrerAuthenticator =>
private case class WikiPageEditForm(
case class WikiPageEditForm(
pageName: String,
content: String,
message: Option[String],
@@ -40,16 +45,16 @@ trait WikiControllerBase extends ControllerBase {
id: String
)
private val newForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pageName, unique))),
val newForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content", text(required, conflictForNew))),
"message" -> trim(label("Message", optional(text()))),
"currentPageName" -> trim(label("Current page name", text())),
"id" -> trim(label("Latest commit id", text()))
)(WikiPageEditForm.apply)
private val editForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pageName))),
val editForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pagename))),
"content" -> trim(label("Content", text(required, conflictForEdit))),
"message" -> trim(label("Message", optional(text()))),
"currentPageName" -> trim(label("Current page name", text(required))),
@@ -57,56 +62,46 @@ trait WikiControllerBase extends ControllerBase {
)(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository =>
val branch = getWikiBranch(repository.owner, repository.name)
getWikiPage(repository.owner, repository.name, "Home", branch).map { page =>
getWikiPage(repository.owner, repository.name, "Home").map { page =>
html.page(
"Home",
page,
getWikiPageList(repository.owner, repository.name, branch),
getWikiPageList(repository.owner, repository.name),
repository,
isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar", branch),
getWikiPage(repository.owner, repository.name, "_Footer", branch)
getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer")
)
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
})
get("/:owner/:repository/wiki/:page")(referrersOnly { repository =>
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(
pageName,
page,
getWikiPageList(repository.owner, repository.name, branch),
getWikiPageList(repository.owner, repository.name),
repository,
isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar", branch),
getWikiPage(repository.owner, repository.name, "_Footer", branch)
getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer")
)
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
})
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
val branch = getWikiBranch(repository.owner, repository.name)
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 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 =>
val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
@@ -116,17 +111,7 @@ trait WikiControllerBase extends ControllerBase {
Some(pageName),
from,
to,
JGitUtil
.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"),
JGitUtil.getDiffs(git, Some(from), to, true, false).filter(_.newPath == pageName + ".md"),
repository,
isEditable(repository),
flash.get("info")
@@ -142,15 +127,7 @@ trait WikiControllerBase extends ControllerBase {
None,
from,
to,
JGitUtil.getDiffs(
git = git,
from = Some(from),
to = to,
fetchContent = true,
makePatch = false,
maxFiles = context.settings.repositoryViewer.maxDiffFiles,
maxLines = context.settings.repositoryViewer.maxDiffLines
),
JGitUtil.getDiffs(git, Some(from), to, true, false),
repository,
isEditable(repository),
flash.get("info")
@@ -159,78 +136,78 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount =>
if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
val branch = getWikiBranch(repository.owner, repository.name)
context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, Some(pageName), branch)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
} else {
flash.update("info", "This patch was not able to be reversed.")
redirect(
s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/$from...$to"
)
}
} else Unauthorized()
if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, Some(pageName))) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
} else {
flash.update("info", "This patch was not able to be reversed.")
redirect(
s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}"
)
}
} else Unauthorized()
}
})
get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount =>
if (isEditable(repository)) {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
val branch = getWikiBranch(repository.owner, repository.name)
context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, None, branch)) {
redirect(s"/${repository.owner}/${repository.name}/wiki")
} else {
flash.update("info", "This patch was not able to be reversed.")
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/$from...$to")
}
} else Unauthorized()
if (revertWikiPage(repository.owner, repository.name, from, to, loginAccount, None)) {
redirect(s"/${repository.owner}/${repository.name}/wiki")
} else {
flash.update("info", "This patch was not able to be reversed.")
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
}
} else Unauthorized()
}
})
get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository =>
if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page"))
val branch = getWikiBranch(repository.owner, repository.name)
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName, branch), repository)
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
} else Unauthorized()
})
post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
if (isEditable(repository)) {
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
loginAccount,
form.message.getOrElse(""),
Some(form.id)
).foreach { commitId =>
updateLastActivityDate(repository.owner, repository.name)
val wikiEditInfo =
EditWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
recordActivity(wikiEditInfo)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) {
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
loginAccount,
form.message.getOrElse(""),
Some(form.id)
).foreach {
commitId =>
updateLastActivityDate(repository.owner, repository.name)
val wikiEditInfo =
EditWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
recordActivity(wikiEditInfo)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
}
}
}
if (notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
} else Unauthorized()
if (notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
} else Unauthorized()
}
})
@@ -241,67 +218,69 @@ trait WikiControllerBase extends ControllerBase {
})
post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { loginAccount =>
if (isEditable(repository)) {
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
form.content,
loginAccount,
form.message.getOrElse(""),
None
).foreach { commitId =>
updateLastActivityDate(repository.owner, repository.name)
val createWikiPageInfo =
CreateWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName)
recordActivity(createWikiPageInfo)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) {
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
form.content,
loginAccount,
form.message.getOrElse(""),
None
).foreach {
commitId =>
updateLastActivityDate(repository.owner, repository.name)
val createWikiPageInfo =
CreateWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName)
recordActivity(createWikiPageInfo)
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
getAccountByUserName(repository.owner).map { repositoryUser =>
WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
}
}
}
if (notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
} else Unauthorized()
if (notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
} else Unauthorized()
}
})
get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository =>
context.withLoginAccount { loginAccount =>
if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page"))
deleteWikiPage(
repository.owner,
repository.name,
pageName,
loginAccount.fullName,
loginAccount.mailAddress,
s"Destroyed $pageName"
)
val deleteWikiInfo = DeleteWikiInfo(
repository.owner,
repository.name,
loginAccount.userName,
pageName
)
recordActivity(deleteWikiInfo)
updateLastActivityDate(repository.owner, repository.name)
context.withLoginAccount {
loginAccount =>
if (isEditable(repository)) {
val pageName = StringUtil.urlDecode(params("page"))
deleteWikiPage(
repository.owner,
repository.name,
pageName,
loginAccount.fullName,
loginAccount.mailAddress,
s"Destroyed ${pageName}"
)
val deleteWikiInfo = DeleteWikiInfo(
repository.owner,
repository.name,
loginAccount.userName,
pageName
)
recordActivity(deleteWikiInfo)
updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki")
} else Unauthorized()
redirect(s"/${repository.owner}/${repository.name}/wiki")
} else Unauthorized()
}
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
val branch = getWikiBranch(repository.owner, repository.name)
html.pages(getWikiPageList(repository.owner, repository.name, branch), repository, isEditable(repository))
html.pages(getWikiPageList(repository.owner, repository.name), repository, isEditable(repository))
})
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
@@ -330,29 +309,24 @@ trait WikiControllerBase extends ControllerBase {
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] = {
val owner = params.value("owner")
val repository = params.value("repository")
val branch = getWikiBranch(owner, repository)
getWikiPageList(owner, repository, branch)
): Option[String] =
getWikiPageList(params.value("owner"), params.value("repository"))
.find(_ == value)
.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] =
if (value.exists("\\/:*?\"<>|".contains(_))) {
Some(s"$name contains invalid character.")
Some(s"${name} contains invalid character.")
} else if (notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))) {
Some(s"$name starts with invalid character.")
Some(s"${name} starts with invalid character.")
} else {
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() {
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] = {
val owner = params("owner")
val repository = params("repository")
val pageName = params("pageName")
val branch = getWikiBranch(owner, repository)
getWikiPage(owner, repository, pageName, branch)
}
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
repository.repository.options.wikiOption match {

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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.service._
import gitbucket.core.util.Implicits._
@@ -7,7 +7,11 @@ import gitbucket.core.util._
import org.scalatra.{Created, NoContent, UnprocessableEntity}
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
@@ -65,24 +69,25 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
JsonFormat(
ApiLabel(
getLabel(repository.owner, repository.name, label.labelId).get,
RepositoryName(repository)
getLabel(repository.owner, repository.name, params("labelName")).map {
label =>
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
JsonFormat(
ApiLabel(
getLabel(repository.owner, repository.name, label.labelId).get,
RepositoryName(repository)
)
)
)
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(
ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(
ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
)
)
)
}
}
} getOrElse NotFound()
}
}) getOrElse NotFound()
@@ -116,10 +121,10 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[AddLabelsToAnIssue]
data <- extractFromJsonBody[Seq[String]]
issueId <- params("id").toIntOpt
} yield {
data.labels.map { labelName =>
data.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel(
repository.owner,
@@ -155,11 +160,11 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[AddLabelsToAnIssue]
data <- extractFromJsonBody[Seq[String]]
issueId <- params("id").toIntOpt
} yield {
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
data.labels.map { labelName =>
data.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel(
repository.owner,
@@ -184,7 +189,7 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
})
/*
* xi Get labels for every issue in a milestone
* https://developer.github.com/v3/issues/labels/#get-labels-for-every-issue-in-a-milestone
*/
* xi Get labels for every issue in a milestone
* https://developer.github.com/v3/issues/labels/#get-labels-for-every-issue-in-a-milestone
*/
}

View File

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

View File

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

View File

@@ -16,8 +16,14 @@ import scala.util.Using
import scala.jdk.CollectionConverters._
trait ApiPullRequestControllerBase extends ControllerBase {
self: AccountService & IssuesService & PullRequestService & RepositoryService & MergeService & ReferrerAuthenticator &
ReadableUsersAuthenticator & WritableUsersAuthenticator =>
self: AccountService
with IssuesService
with PullRequestService
with RepositoryService
with MergeService
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator =>
/*
* i. Link Relations
@@ -42,18 +48,19 @@ trait ApiPullRequestControllerBase extends ControllerBase {
repos = repository.owner -> repository.name
)
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
assignees = assignees.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
JsonFormat(issues.map {
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
assignees = assignees.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
})
@@ -82,37 +89,38 @@ trait ApiPullRequestControllerBase extends ControllerBase {
case Left(createPullReq) =>
val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReq.head, repository.owner)
getRepository(reqOwner, repository.name)
.flatMap { forkedRepository =>
getPullRequestCommitFromTo(repository, forkedRepository, createPullReq.base, reqBranch) match {
case (Some(commitIdFrom), Some(commitIdTo)) =>
val issueId = insertIssue(
owner = repository.owner,
repository = repository.name,
loginUser = context.loginAccount.get.userName,
title = createPullReq.title,
content = createPullReq.body,
milestoneId = None,
priorityId = None,
isPullRequest = true
)
.flatMap {
forkedRepository =>
getPullRequestCommitFromTo(repository, forkedRepository, createPullReq.base, reqBranch) match {
case (Some(commitIdFrom), Some(commitIdTo)) =>
val issueId = insertIssue(
owner = repository.owner,
repository = repository.name,
loginUser = context.loginAccount.get.userName,
title = createPullReq.title,
content = createPullReq.body,
milestoneId = None,
priorityId = None,
isPullRequest = true
)
createPullRequest(
originRepository = repository,
issueId = issueId,
originBranch = createPullReq.base,
requestUserName = reqOwner,
requestRepositoryName = repository.name,
requestBranch = reqBranch,
commitIdFrom = commitIdFrom.getName,
commitIdTo = commitIdTo.getName,
isDraft = createPullReq.draft.getOrElse(false),
loginAccount = context.loginAccount.get,
settings = context.settings
)
getApiPullRequest(repository, issueId).map(JsonFormat(_))
case _ =>
None
}
createPullRequest(
originRepository = repository,
issueId = issueId,
originBranch = createPullReq.base,
requestUserName = reqOwner,
requestRepositoryName = repository.name,
requestBranch = reqBranch,
commitIdFrom = commitIdFrom.getName,
commitIdTo = commitIdTo.getName,
isDraft = createPullReq.draft.getOrElse(false),
loginAccount = context.loginAccount.get,
settings = context.settings
)
getApiPullRequest(repository, issueId).map(JsonFormat(_))
case _ =>
None
}
}
.getOrElse {
NotFound()
@@ -120,27 +128,28 @@ trait ApiPullRequestControllerBase extends ControllerBase {
case Right(createPullReqAlt) =>
val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReqAlt.head, repository.owner)
getRepository(reqOwner, repository.name)
.flatMap { forkedRepository =>
getPullRequestCommitFromTo(repository, forkedRepository, createPullReqAlt.base, reqBranch) match {
case (Some(commitIdFrom), Some(commitIdTo)) =>
changeIssueToPullRequest(repository.owner, repository.name, createPullReqAlt.issue)
createPullRequest(
originRepository = repository,
issueId = createPullReqAlt.issue,
originBranch = createPullReqAlt.base,
requestUserName = reqOwner,
requestRepositoryName = repository.name,
requestBranch = reqBranch,
commitIdFrom = commitIdFrom.getName,
commitIdTo = commitIdTo.getName,
isDraft = false,
loginAccount = context.loginAccount.get,
settings = context.settings
)
getApiPullRequest(repository, createPullReqAlt.issue).map(JsonFormat(_))
case _ =>
None
}
.flatMap {
forkedRepository =>
getPullRequestCommitFromTo(repository, forkedRepository, createPullReqAlt.base, reqBranch) match {
case (Some(commitIdFrom), Some(commitIdTo)) =>
changeIssueToPullRequest(repository.owner, repository.name, createPullReqAlt.issue)
createPullRequest(
originRepository = repository,
issueId = createPullReqAlt.issue,
originBranch = createPullReqAlt.base,
requestUserName = reqOwner,
requestRepositoryName = repository.name,
requestBranch = reqBranch,
commitIdFrom = commitIdFrom.getName,
commitIdTo = commitIdTo.getName,
isDraft = false,
loginAccount = context.loginAccount.get,
settings = context.settings
)
getApiPullRequest(repository, createPullReqAlt.issue).map(JsonFormat(_))
case _ =>
None
}
}
.getOrElse {
NotFound()
@@ -181,24 +190,26 @@ trait ApiPullRequestControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
params("id").toIntOpt.flatMap { issueId =>
getPullRequest(owner, name, issueId) map { case (issue, pullreq) =>
Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
val newId = git.getRepository.resolve(pullreq.commitIdTo)
val repoFullName = RepositoryName(repository)
val commits = git.log
.addRange(oldId, newId)
.call
.iterator
.asScala
.map { c =>
ApiCommitListItem(new CommitInfo(c), repoFullName)
params("id").toIntOpt.flatMap {
issueId =>
getPullRequest(owner, name, issueId) map {
case (issue, pullreq) =>
Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
val newId = git.getRepository.resolve(pullreq.commitIdTo)
val repoFullName = RepositoryName(repository)
val commits = git.log
.addRange(oldId, newId)
.call
.iterator
.asScala
.map { c =>
ApiCommitListItem(new CommitInfo(c), repoFullName)
}
.toList
JsonFormat(commits)
}
.toList
JsonFormat(commits)
}
}
} getOrElse NotFound()
})
/*
@@ -229,8 +240,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
*/
put("/api/v3/repos/:owner/:repository/pulls/:id/merge")(referrersOnly { repository =>
(for {
// TODO: crash when body is empty
// TODO: Implement sha parameter
//TODO: crash when body is empty
//TODO: Implement sha parameter
data <- extractFromJsonBody[MergeAPullRequest]
issueId <- params("id").toIntOpt
(issue, pullReq) <- getPullRequest(repository.owner, repository.name, issueId)
@@ -262,7 +273,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
repository,
issueId,
context.loginAccount.get,
data.commit_message.getOrElse(""), // TODO: Implement commit_title
data.commit_message.getOrElse(""), //TODO: Implement commit_title
strategy,
pullReq.isDraft,
context.settings

View File

@@ -11,7 +11,7 @@ import org.apache.commons.io.FileUtils
import org.scalatra.NoContent
trait ApiReleaseControllerBase extends ControllerBase {
self: AccountService & ReleaseService & ReferrerAuthenticator & WritableUsersAuthenticator =>
self: AccountService with ReleaseService with ReferrerAuthenticator with WritableUsersAuthenticator =>
/**
* i. List releases for a repository
@@ -119,39 +119,40 @@ trait ApiReleaseControllerBase extends ControllerBase {
* ix. 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 =>
val name = params("name")
val tag = params("tag")
getRelease(repository.owner, repository.name, tag)
.map { release =>
val fileId = FileUtil.generateFileId
val buf = new Array[Byte](request.inputStream.available())
request.inputStream.read(buf)
FileUtils.writeByteArrayToFile(
new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(tag + "/" + fileId)
),
buf
)
createReleaseAsset(
repository.owner,
repository.name,
tag,
fileId,
name,
request.contentLength.getOrElse(0),
context.loginAccount.get
)
getReleaseAsset(repository.owner, repository.name, tag, fileId)
.map { asset =>
JsonFormat(ApiReleaseAsset(asset, RepositoryName(repository)))
}
.getOrElse {
ApiError("Unknown error")
}
}
.getOrElse(NotFound())
post("/api/v3/repos/:owner/:repository/releases/:tag/assets")(writableUsersOnly {
repository =>
val name = params("name")
val tag = params("tag")
getRelease(repository.owner, repository.name, tag)
.map { release =>
val fileId = FileUtil.generateFileId
val buf = new Array[Byte](request.inputStream.available())
request.inputStream.read(buf)
FileUtils.writeByteArrayToFile(
new File(
getReleaseFilesDir(repository.owner, repository.name),
FileUtil.checkFilename(tag + "/" + fileId)
),
buf
)
createReleaseAsset(
repository.owner,
repository.name,
tag,
fileId,
name,
request.contentLength.getOrElse(0),
context.loginAccount.get
)
getReleaseAsset(repository.owner, repository.name, tag, fileId)
.map { asset =>
JsonFormat(ApiReleaseAsset(asset, RepositoryName(repository)))
}
.getOrElse {
ApiError("Unknown error")
}
}
.getOrElse(NotFound())
})
/**
@@ -175,7 +176,7 @@ trait ApiReleaseControllerBase extends ControllerBase {
*/
/*
* xii. Delete a release asset
* https://developer.github.com/v3/repos/releases/#edit-a-release-asset
*/
* xii. Delete a release asset
* https://developer.github.com/v3/repos/releases/#edit-a-release-asset
*/
}

View File

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

View File

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

View File

@@ -4,24 +4,31 @@ import gitbucket.core.controller.ControllerBase
import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, CommitsService, ProtectedBranchService}
import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.JGitUtil.{CommitInfo, getBranchesNoMergeInfo, getBranchesOfCommit}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil.{CommitInfo, getBranches, getBranchesOfCommit}
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
import org.eclipse.jgit.api.Git
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 java.time.format.DateTimeFormatter.*
import java.time.{LocalDateTime, ZoneOffset}
import java.util.Date
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters.*
import scala.math.min
import scala.jdk.CollectionConverters._
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 {
self: AccountService & CommitsService & ProtectedBranchService & ReferrerAuthenticator =>
self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator =>
/*
* i. List commits on a repository
* https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
@@ -36,53 +43,56 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
val path = params.get("path").filter(_.nonEmpty)
val since = params.get("since").filter(_.nonEmpty)
val until = params.get("until").filter(_.nonEmpty)
Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
val repo = git.getRepository
Using.resource(new RevWalk(repo)) { revWalk =>
val objectId = repo.resolve(sha)
revWalk.markStart(revWalk.parseCommit(objectId))
if (path.nonEmpty) {
revWalk.setTreeFilter(
AndTreeFilter.create(PathFilterGroup.createFromStrings(path.get), TreeFilter.ANY_DIFF)
)
Using.resource(Git.open(getRepositoryDir(owner, name))) {
git =>
val repo = git.getRepository
Using.resource(new RevWalk(repo)) {
revWalk =>
val objectId = repo.resolve(sha)
revWalk.markStart(revWalk.parseCommit(objectId))
if (path.nonEmpty) {
revWalk.setTreeFilter(
AndTreeFilter.create(PathFilterGroup.createFromStrings(path.get), TreeFilter.ANY_DIFF)
)
}
val revfilters = new ListBuffer[(RevFilter)]()
if (author.nonEmpty) {
revfilters += AuthorRevFilter.create(author.get)
}
if (since.nonEmpty) {
revfilters += CommitTimeRevFilter.after(
Date.from(LocalDateTime.parse(since.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (until.nonEmpty) {
revfilters += CommitTimeRevFilter.before(
Date.from(LocalDateTime.parse(until.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (page > 1) {
revfilters += SkipRevFilter.create(page * per_page - 2)
}
revfilters += MaxCountRevFilter.create(per_page);
revWalk.setRevFilter(
if (revfilters.size > 1) {
AndRevFilter.create(revfilters.toArray)
} else {
revfilters(0)
}
)
JsonFormat(revWalk.asScala.map {
commit =>
val commitInfo = new CommitInfo(commit)
ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, commitInfo.id).size
)
})
}
val revfilters = new ListBuffer[(RevFilter)]()
if (author.nonEmpty) {
revfilters += AuthorRevFilter.create(author.get)
}
if (since.nonEmpty) {
revfilters += CommitTimeRevFilter.after(
Date.from(LocalDateTime.parse(since.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (until.nonEmpty) {
revfilters += CommitTimeRevFilter.before(
Date.from(LocalDateTime.parse(until.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (page > 1) {
revfilters += SkipRevFilter.create(page * per_page - 2)
}
revfilters += MaxCountRevFilter.create(per_page);
revWalk.setRevFilter(
if (revfilters.size > 1) {
AndRevFilter.create(revfilters.toArray)
} else {
revfilters(0)
}
)
JsonFormat(revWalk.asScala.map { commit =>
val commitInfo = new CommitInfo(commit)
ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, commitInfo.id).size
)
})
}
}
})
@@ -95,23 +105,24 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
val name = repository.name
val sha = params("sha")
Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
val repo = git.getRepository
val objectId = repo.resolve(sha)
val commitInfo = Using.resource(new RevWalk(repo)) { revWalk =>
new CommitInfo(revWalk.parseCommit(objectId))
}
Using.resource(Git.open(getRepositoryDir(owner, name))) {
git =>
val repo = git.getRepository
val objectId = repo.resolve(sha)
val commitInfo = Using.resource(new RevWalk(repo)) { revWalk =>
new CommitInfo(revWalk.parseCommit(objectId))
}
JsonFormat(
ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
JsonFormat(
ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
)
)
)
}
})
@@ -159,7 +170,7 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val apiBranchForCommits = for {
branch <- getBranchesOfCommit(git, sha)
br <- getBranchesNoMergeInfo(git).find(_.name == branch)
br <- getBranches(git, branch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
ApiBranchForHeadCommit(branch, ApiBranchCommit(br.commitId), protection.enabled)

View File

@@ -12,23 +12,24 @@ import org.eclipse.jgit.api.Git
import scala.util.Using
trait ApiRepositoryContentsControllerBase extends ControllerBase {
self: ReferrerAuthenticator & WritableUsersAuthenticator & RepositoryCommitFileService =>
self: ReferrerAuthenticator with WritableUsersAuthenticator with RepositoryCommitFileService =>
/**
* i. 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 =>
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
val files = getFileList(git, refStr, ".", maxFiles = context.settings.repositoryViewer.maxFiles)
files // files should be sorted alphabetically.
.find { file =>
!file.isDirectory && RepositoryService.readmeFiles.contains(file.name.toLowerCase)
} match {
case Some(x) => getContents(repository = repository, path = x.name, refStr = refStr, ignoreCase = true)
case _ => NotFound()
}
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) {
git =>
val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
val files = getFileList(git, refStr, ".", maxFiles = context.settings.repositoryViewer.maxFiles)
files // files should be sorted alphabetically.
.find { file =>
!file.isDirectory && RepositoryService.readmeFiles.contains(file.name.toLowerCase)
} match {
case Some(x) => getContents(repository = repository, path = x.name, refStr = refStr, ignoreCase = true)
case _ => NotFound()
}
}
})
@@ -133,65 +134,67 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
* requested #2112
*/
put("/api/v3/repos/:owner/:repository/contents/*")(writableUsersOnly { repository =>
context.withLoginAccount { loginAccount =>
JsonFormat(for {
data <- extractFromJsonBody[CreateAFile]
} yield {
val branch = data.branch.getOrElse(repository.repository.defaultBranch)
val commit = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
revCommit.name
}
val paths = multiParams("splat").head.split("/")
val path = paths.take(paths.size - 1).toList.mkString("/")
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val fileInfo = getFileInfo(git, commit, path, false)
context.withLoginAccount {
loginAccount =>
JsonFormat(for {
data <- extractFromJsonBody[CreateAFile]
} yield {
val branch = data.branch.getOrElse(repository.repository.defaultBranch)
val commit = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
revCommit.name
}
val paths = multiParams("splat").head.split("/")
val path = paths.take(paths.size - 1).toList.mkString("/")
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) {
git =>
val fileInfo = getFileInfo(git, commit, path, false)
fileInfo match {
case Some(f) if !data.sha.contains(f.id.getName) =>
ApiError(
"The blob SHA is not matched.",
Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
)
case _ =>
commitFile(
repository,
branch,
path,
Some(paths.last),
data.sha.map(_ => paths.last),
StringUtil.base64Decode(data.content),
data.message,
commit,
loginAccount,
data.committer.map(_.name).getOrElse(loginAccount.fullName),
data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
context.settings
) match {
case Left(error) =>
ApiError(s"Failed to commit a file: ${error}", None)
case Right((_, None)) =>
ApiError("Failed to commit a file.", None)
case Right((commitId, Some(blobId))) =>
Map(
"content" -> ApiContents(
"file",
paths.last,
path,
blobId.name,
Some(data.content),
Some("base64")
)(RepositoryName(repository)),
"commit" -> ApiCommit(
git,
RepositoryName(repository),
new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
)
fileInfo match {
case Some(f) if !data.sha.contains(f.id.getName) =>
ApiError(
"The blob SHA is not matched.",
Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
)
case _ =>
commitFile(
repository,
branch,
path,
Some(paths.last),
data.sha.map(_ => paths.last),
StringUtil.base64Decode(data.content),
data.message,
commit,
loginAccount,
data.committer.map(_.name).getOrElse(loginAccount.fullName),
data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
context.settings
) match {
case Left(error) =>
ApiError(s"Failed to commit a file: ${error}", None)
case Right((_, None)) =>
ApiError("Failed to commit a file.", None)
case Right((commitId, Some(blobId))) =>
Map(
"content" -> ApiContents(
"file",
paths.last,
path,
blobId.name,
Some(data.content),
Some("base64")
)(RepositoryName(repository)),
"commit" -> ApiCommit(
git,
RepositoryName(repository),
new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
)
)
}
}
}
}
})
})
}
})
@@ -202,9 +205,9 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
*/
/*
* vi. Download a repository archive (tar/zip)
* https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-tar
* https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-zip
*/
* vi. Download a repository archive (tar/zip)
* https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-tar
* https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-zip
*/
}

View File

@@ -15,9 +15,16 @@ import scala.concurrent.duration.Duration
import scala.util.Using
trait ApiRepositoryControllerBase extends ControllerBase {
self: RepositoryService & ApiGitReferenceControllerBase & RepositoryCreationService & AccountService &
OwnerAuthenticator & UsersAuthenticator & GroupManagerAuthenticator & ReferrerAuthenticator &
ReadableUsersAuthenticator & WritableUsersAuthenticator =>
self: RepositoryService
with ApiGitReferenceControllerBase
with RepositoryCreationService
with AccountService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator =>
/**
* i. List your repositories
@@ -86,8 +93,7 @@ trait ApiRepositoryControllerBase extends ControllerBase {
data.name,
data.description,
data.`private`,
data.auto_init,
context.settings.defaultBranch
data.auto_init
)
Await.result(f, Duration.Inf)
@@ -124,8 +130,7 @@ trait ApiRepositoryControllerBase extends ControllerBase {
data.name,
data.description,
data.`private`,
data.auto_init,
context.settings.defaultBranch
data.auto_init
)
Await.result(f, Duration.Inf)
val repository = Database() withTransaction { session =>
@@ -182,7 +187,7 @@ trait ApiRepositoryControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
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}
trait ApiRepositoryStatusControllerBase extends ControllerBase {
self: AccountService & CommitStatusService & ReferrerAuthenticator & WritableUsersAuthenticator =>
self: AccountService with CommitStatusService with ReferrerAuthenticator with WritableUsersAuthenticator =>
/*
* i. Create a status
@@ -47,8 +47,9 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase {
ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
JsonFormat(getCommitStatusesWithCreator(repository.owner, repository.name, sha).map { case (status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
JsonFormat(getCommitStatusesWithCreator(repository.owner, repository.name, sha).map {
case (status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
})
}) getOrElse NotFound()
})

View File

@@ -8,7 +8,7 @@ import gitbucket.core.util.Implicits._
import org.scalatra.NoContent
trait ApiRepositoryWebhookControllerBase extends ControllerBase {
self: RepositoryService & WebHookService & ReferrerAuthenticator & WritableUsersAuthenticator =>
self: RepositoryService with WebHookService with ReferrerAuthenticator with WritableUsersAuthenticator =>
/*
* i. List repository webhooks
@@ -113,8 +113,8 @@ trait ApiRepositoryWebhookControllerBase extends ControllerBase {
*/
/*
* vi. Test the push repository webhook
* https://docs.github.com/en/rest/reference/repos#test-the-push-repository-webhook
*/
* vi. Test the push repository webhook
* https://docs.github.com/en/rest/reference/repos#test-the-push-repository-webhook
*/
}

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ trait AccountComponent { self: Profile =>
groupAccount,
removed,
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 extraMailAddress = column[String]("EXTRA_MAIL_ADDRESS", O PrimaryKey)
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 subject = column[String]("SUBJECT")
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] =
(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 highlighterTheme = column[String]("HIGHLIGHTER_THEME")
def * =
(userName, highlighterTheme).mapTo[AccountPreference]
(userName, highlighterTheme).<>(AccountPreference.tupled, AccountPreference.unapply)
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 token = column[Option[String]]("TOKEN")
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)
}

View File

@@ -12,7 +12,7 @@ trait AccountWebHookEventComponent extends TemplateComponent { self: Profile =>
val url = column[String]("URL")
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)

View File

@@ -11,7 +11,7 @@ trait ActivityComponent extends TemplateComponent { self: Profile =>
lazy val Activities = TableQuery[Activities]
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 =>
import profile.api._
trait BasicTemplate { self: Table[?] =>
trait BasicTemplate { self: Table[_] =>
val userName = column[String]("USER_NAME")
val repositoryName = column[String]("REPOSITORY_NAME")
@@ -18,7 +18,7 @@ protected[model] trait TemplateComponent { self: Profile =>
(this.userName === userName) && (this.repositoryName === repositoryName)
}
trait IssueTemplate extends BasicTemplate { self: Table[?] =>
trait IssueTemplate extends BasicTemplate { self: Table[_] =>
val issueId = column[Int]("ISSUE_ID")
def byIssue(owner: String, repository: String, issueId: Int) =
@@ -28,7 +28,7 @@ protected[model] trait TemplateComponent { self: Profile =>
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 labelName = column[String]("LABEL_NAME")
@@ -42,7 +42,7 @@ protected[model] trait TemplateComponent { self: Profile =>
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 priorityName = column[String]("PRIORITY_NAME")
@@ -56,7 +56,7 @@ protected[model] trait TemplateComponent { self: Profile =>
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")
def byMilestone(owner: String, repository: String, milestoneId: Int) =
@@ -66,7 +66,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
}
trait CommitTemplate extends BasicTemplate { self: Table[?] =>
trait CommitTemplate extends BasicTemplate { self: Table[_] =>
val commitId = column[String]("COMMIT_ID")
def byCommit(owner: String, repository: String, commitId: String) =
@@ -76,7 +76,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.commitId === commitId)
}
trait BranchTemplate extends BasicTemplate { self: Table[?] =>
trait BranchTemplate extends BasicTemplate { self: Table[_] =>
val branch = column[String]("BRANCH")
def byBranch(owner: String, repository: String, branchName: String) =
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 {
val collaboratorName = column[String]("COLLABORATOR_NAME")
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) =
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")
def * =
(userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate)
.mapTo[IssueComment]
.<>(IssueComment.tupled, IssueComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}
@@ -75,7 +75,7 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile =>
originalCommitId,
originalOldLine,
originalNewLine
).mapTo[CommitComment]
).<>(CommitComment.tupled, CommitComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}

View File

@@ -30,7 +30,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile =>
creator,
registeredDate,
updatedDate
).mapTo[CommitStatus]
).<>((CommitStatus.apply _).tupled, CommitStatus.unapply)
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.view.helpers
import org.scalatra.i18n.Messages
import play.twirl.api.Html
trait CustomFieldComponent extends TemplateComponent { self: Profile =>
import profile.api._
@@ -16,12 +15,11 @@ trait CustomFieldComponent extends TemplateComponent { self: Profile =>
val fieldId = column[Int]("FIELD_ID", O AutoInc)
val fieldName = column[String]("FIELD_NAME")
val fieldType = column[String]("FIELD_TYPE")
val constraints = column[Option[String]]("CONSTRAINTS")
val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES")
val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS")
def * =
(userName, repositoryName, fieldId, fieldName, fieldType, constraints, enableForIssues, enableForPullRequests)
.mapTo[CustomField]
(userName, repositoryName, fieldId, fieldName, fieldType, enableForIssues, enableForPullRequests)
.<>(CustomField.tupled, CustomField.unapply)
def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) =
(this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.fieldId === fieldId.bind)
@@ -33,28 +31,17 @@ case class CustomField(
repositoryName: String,
fieldId: Int = 0,
fieldName: String,
fieldType: String, // long, double, string, date, or enum
constraints: Option[String],
fieldType: String, // long, double, string, or date
enableForIssues: Boolean,
enableForPullRequests: Boolean
)
trait CustomFieldBehavior {
def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(implicit
context: Context
def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit conext: Context): String
def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
implicit context: Context
): String
def fieldHtml(
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]
def validate(name: String, value: String, messages: Messages): Option[String]
}
object CustomFieldBehavior {
@@ -62,7 +49,7 @@ object CustomFieldBehavior {
if (value.isEmpty) None
else {
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 "string" => Some(StringFieldBehavior)
case "date" => Some(DateFieldBehavior)
case "enum" => Some(EnumFieldBehavior)
case _ => None
}
}
case object LongFieldBehavior extends TextFieldBehavior {
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
value.toLong
None
@@ -94,12 +75,7 @@ object CustomFieldBehavior {
}
}
case object DoubleFieldBehavior extends TextFieldBehavior {
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
value.toDouble
None
@@ -113,12 +89,7 @@ object CustomFieldBehavior {
private val pattern = "yyyy-MM-dd"
override protected val fieldType: String = "date"
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
new java.text.SimpleDateFormat(pattern).parse(value)
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 {
protected val fieldType = "text"
override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
implicit context: Context
): String = {
def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit context: Context): String = {
val sb = new StringBuilder
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;"/>"""
@@ -270,7 +111,8 @@ object CustomFieldBehavior {
sb.append(s"""<script>
|$$('#custom-field-$fieldId').focusout(function(){
| 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() },
| function(data){
| if (data != '') {
@@ -286,34 +128,14 @@ object CustomFieldBehavior {
sb.toString()
}
override def fieldHtml(
repository: RepositoryInfo,
issueId: Int,
fieldId: Int,
fieldName: String,
constraints: Option[String],
value: String,
editable: Boolean
)(implicit
context: Context
def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
implicit context: Context
): String = {
val sb = new StringBuilder
if (value.nonEmpty) {
sb.append(
s"""<span id="custom-field-$fieldId-label" class="custom-field-label">${StringUtil
.escapeHtml(value)}</span>"""
)
} 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>"""
)
}
}
sb.append(
s"""<span id="custom-field-$fieldId-label" class="custom-field-label">${StringUtil
.escapeHtml(value)}</span>""".stripMargin
)
if (editable) {
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;"/>"""
@@ -327,22 +149,19 @@ object CustomFieldBehavior {
|
|$$('#custom-field-$fieldId-editor').focusout(function(){
| 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() },
| function(data){
| if (data != '') {
| $$('#custom-field-$fieldId-error').text(data);
| } else {
| $$('#custom-field-$fieldId-error').text('');
| $$.post('${helpers.url(repository)}/issues/$issueId/customfield/$fieldId',
| $$.post('${helpers.url(repository)}/issues/$issueId/customfield/' + fieldId,
| { value: $$this.val() },
| function(data){
| $$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()
}
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = None
def validate(name: 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 allowWrite = column[Boolean]("ALLOW_WRITE")
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) =
(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 title = column[String]("TITLE")
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) =
(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 userName = column[String]("USER_NAME", O PrimaryKey)
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,
updatedDate,
pullRequest
).mapTo[Issue]
).<>(Issue.tupled, Issue.unapply)
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 {
val assigneeUserName = column[String]("ASSIGNEE_USER_NAME")
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) = {
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 value = column[String]("VALUE")
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) = {
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]
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) =
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 labelName = column[String]("LABEL_NAME")
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(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 closedDate = column[Option[java.util.Date]]("CLOSED_DATE")
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(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 color = column[String]("COLOR")
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(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) =

View File

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