Compare commits

..

6 Commits

Author SHA1 Message Date
Naoki Takezoe
e6f0882b77 (refs #1805) Fix get-blame API response 2017-12-13 00:31:56 +09:00
Naoki Takezoe
976202be0a Update README.md 2017-12-07 01:15:51 +09:00
Naoki Takezoe
a07576525c Bump to 4.19.3 2017-12-06 14:41:48 +09:00
Naoki Takezoe
f9c3607370 Fix comment reply form behavior in the diff view (#1796) 2017-12-06 14:39:25 +09:00
Naoki Takezoe
7078f32d1c Fix diff class attribute in split mode 2017-12-06 14:39:19 +09:00
Naoki Takezoe
c51fd7a7cd Fix file uploading issue 2017-12-06 14:39:09 +09:00
744 changed files with 145703 additions and 185462 deletions

View File

@@ -1,8 +1,8 @@
### Before submitting an issue to GitBucket I have first: ### Before submitting an issue to GitBucket I have first:
- [ ] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md) - [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [ ] searched for similar already existing issue - [] searched for similar already existing issue
- [ ] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki) - [] 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)* *(if you have performed all the above, remove the paragraph and continue describing the issue with template below)*

View File

@@ -1,8 +1,8 @@
### Before submitting a pull-request to GitBucket I have first: ### Before submitting a pull-request to GitBucket I have first:
- [ ] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md) - [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [ ] rebased my branch over master - [] rebased my branch over master
- [ ] verified that project is compiling - [] verified that project is compiling
- [ ] verified that tests are passing - [] verified that tests are passing
- [ ] squashed my commits as appropriate *(keep several commits if it is relevant to understand the PR)* - [] squashed my commits as appropriate *(keep several commits if it is relevant to understand the PR)*
- [ ] [marked as closed using commit message](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct - [] [marked as closed using commit message](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct

View File

@@ -1,29 +0,0 @@
name: build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [8, 11]
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}
- name: Run tests
run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
- name: Build executable
run: sbt executable
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
path: ./target/executable/gitbucket.*

5
.gitignore vendored
View File

@@ -24,8 +24,3 @@ project/plugins/project/
.idea/ .idea/
.idea_modules/ .idea_modules/
*.iml *.iml
# Metals specific
.metals
.bloop
project/metals.sbt

View File

@@ -1,12 +0,0 @@
version = "1.5.1"
project.git = true
maxColumn = 120
docstrings = JavaDoc
align.tokens = ["%", "%%", {code = "=>", owner = "Case"}]
align.openParenCallSite = false
align.openParenDefnSite = false
continuationIndent.callSite = 2
continuationIndent.defnSite = 2
danglingParentheses = true

18
.travis.yml Normal file
View File

@@ -0,0 +1,18 @@
language: scala
sudo: true
jdk:
- oraclejdk8
script:
- sbt test
before_script:
- sudo /etc/init.d/mysql stop
- sudo /etc/init.d/postgresql stop
- sudo chmod +x /usr/local/bin/sbt
cache:
directories:
- $HOME/.ivy2/cache
- $HOME/.sbt/boot
- $HOME/.sbt/launchers
- $HOME/.coursier
- $HOME/.embedmysql
- $HOME/.embedpostgresql

View File

@@ -1,143 +1,6 @@
# Changelog # Changelog
All changes to the project will be documented in this file. All changes to the project will be documented in this file.
### 4.33.0 - 31 Dec 2019
- All CLI options are configurable by environment variables
- Folding pull request files
- WebHook security options
- Add assignee and assignees properties to some Web APIs' response
### 4.32.0 - 7 Aug 2019
- Bump to Scala 2.13.0 and Scalatra 2.7.0
- Draft pull request
- Drop network installation of plugins
- Compare view works for commit id
- Apply default priority to pull requests
- Focus title after clicking issue / pull request edit button
### 4.31.2 - 7 Apr 2019
- Bug and security fix
### 4.31.1 - 17 Mar 2019
- Bug fix
### 4.31.0 - 17 Mar 2019
- Docker support in CI plugin
- Verify GPG key signed commit
- OAuth2 Token (sent as a parameter) authentication support and new APIs in Web API
- OGP (Open Graph protocol) support
- Username completion with avatars
### 4.30.1 - 22 Dec 2018
- Bug fix for several WebHooks and Web API
## 4.30.0 - 15 Dec 2018
- Automatic ChangeLog Summary generation for new Releases
- A lot of GitBucket Web API updates to increase compatibility with the GitHub API.
- Display of checkboxes in Markdown files in Git repositories
- A new extension point for plugins: anonymousAccessiblePaths
- Group support in the Gist Plugin
- Allow redirection to the Release Page from the Activity Timeline Page
## 4.29.0 - 29 Sep 2018
- Official Docker image has been available
- Enhance file edit and delete buttons of the repository viewer
- Fix Patch button to generate patches for all files in the commit
- Display confirmation dialog for Transfer Ownership and Garbage collection
- Fix wrong url encoding in "Compare & pull request"
## 4.28.0 - 1 Sep 2018
- Proxy support for plugin installation
- Fix some bugs around pull requests
## 4.27.0 - 29 Jul 2018
- Create new tag on the browser
- EditorConfig support
- Improve issues / pull requests search
- Some improvements and bug fixes for plugin installation via internet and pull request commenting
## 4.26.0 - 30 Jun 2018
- Installing plugins from the central registry
- Repositories tab in the dashboard
- Fork dialog enhancement
- Adjust pull request creation suggestor
- Keep showing incompleted task list
- New notification hooks
## 4.25.0 - 29 May 2018
- Security improvements
- Show mail address at the profile page
- Task list on commit comments
- More detailed editing history of issues and pull requests
- Expose user public keys
- Download repository improvements
## 4.24.1 - 1 May 2018
- Fix bug in Web API authentication
## 4.24.0 - 30 Apr 2018
- Diff for each review comment on pull requests
- Extra mail addresses support
- Show tags at the commit list
- Keep wrap mode of the online editor
- Renew layout of gitbucket-gist-plugin
- Web API of gitbucket-ci-plugin
## 4.23.1 - 10 Apr 2018
- Fix bug that the contents API doesn't work for the repository root
- Fix shutdown problem in Tomcat deployment
- Render by plugins at the blob view even if it's a binary file
## 4.23.0 - 31 Mar 2018
- Allow tail slash in URL
- Display commit message of tags at the releases page
- Add labels property to issues and pull requests API response
- Plugins list API
- Git authentication with personal access token
- Max parallel builds and max stored history in CI plugin became configurable
## 4.22.0 - 3 Mar 2018
- Pull request merge strategy settings
- Create repository with an empty commit
- Improve database viewer
- Update maven-repository-plugin
## 4.21.2 - 27 Jan 2018
- Bugfix
## 4.21.1 - 27 Jan 2018
- Bugfix
## 4.21.0 - 27 Jan 2018
- Release page
- OpenID Connect support
- New database viewer
- Submodule links to web page
- Clarify close/reopen button
# 4.20.0 - 23 Dec 2017
- Squash and rebase merge strategy for pull requests
- Quick pull request creation
- Download patch from the diff view
- Fork and create repository are proceeded asynchronously
- Create new repository by copying existing git repository
- Hide overflowed repository names in the sidebar
- Support CreateEvent web hook
- Display conflicting files if pull request can't be merged
## 4.19.3 - 7 Dec 2017
- Fix file uploading bug
- Fix reply comment form behavior in the diff view
## 4.19.2 - 3 Dec 2017
- Fix routing bug in `CompositeScalatraFilter`
- Resolve id attribute collision in the web hook editing form
## 4.19.1 - 2 Dec 2017
- Update gitbucket-notifications-plugin because it had a version compatibility issue
## 4.19.0 - 2 Dec 2017 ## 4.19.0 - 2 Dec 2017
- [gitbucket-maven-repository-plugin](https://github.com/takezoe/gitbucket-maven-repository-plugin) is available - [gitbucket-maven-repository-plugin](https://github.com/takezoe/gitbucket-maven-repository-plugin) is available
- Upgrade to Scalatra 2.6 - Upgrade to Scalatra 2.6

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/workflows/build/badge.svg?branch=master)](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.gitbucket/gitbucket_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.gitbucket/gitbucket_2.13) [![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.png)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket)
========= =========
GitBucket is a Git web platform powered by Scala offering: GitBucket is a Git web platform powered by Scala offering:
@@ -22,7 +22,7 @@ The current version of GitBucket provides many features such as:
- Account and group management with LDAP integration - Account and group management with LDAP integration
- a Plug-in system - a Plug-in system
If you want to try the development version of GitBucket, see the [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/readme.md). If you want to try the development version of GitBucket, see the [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
Installation Installation
-------- --------
@@ -31,6 +31,19 @@ GitBucket requires **Java8**. You have to install it, if it is not already insta
1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`. 1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**. 2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
You can specify following options:
- `--port=[NUMBER]`
- `--prefix=[CONTEXTPATH]`
- `--host=[HOSTNAME]`
- `--gitbucket.home=[DATA_DIR]`
- `--temp_dir=[TEMP_DIR]`
- `--max_file_size=[MAX_FILE_SIZE]`
`TEMP_DIR` is used as the [temporary directory for the jetty application context](https://www.eclipse.org/jetty/documentation/9.3.x/ref-temporary-directories.html). This is the directory into which the `gitbucket.war` file is unpacked, the source files are compiled, etc. If given this parameter **must** match the path of an existing directory or the application will quit reporting an error; if not given the path used will be a `tmp` directory inside the gitbucket home.
`MAX_FILE_SIZE` is the max file size for upload files.
You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc) You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc)
For more information about installation on Mac or Windows Server (with IIS), or configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki). For more information about installation on Mac or Windows Server (with IIS), or configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
@@ -55,13 +68,29 @@ Support
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. - If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. - The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
What's New in 4.33.x What's New in 4.19.x
------------- -------------
### 4.33.0 - 31 Dec 2019
- All CLI options are configurable by environment variables ### 4.19.3 - 7 Dec 2017
- Folding pull request files
- WebHook security options - Fix file uploading bug
- Add assignee and assignees properties to some Web APIs' response - Fix reply comment form behavior in the diff view
### 4.19.2 - 3 Dec 2017
- Fix routing bug in `CompositeScalatraFilter`
- Resolve id attribute collision in the web hook editing form
### 4.19.1 - 2 Dec 2017
- Update gitbucket-notifications-plugin because it had a version compatibility issue
### 4.19.0 - 2 Dec 2017
- [gitbucket-maven-repository-plugin](https://github.com/takezoe/gitbucket-maven-repository-plugin) is available
- Upgrade to Scalatra 2.6
- Improve layout of the system settings page
- New extension point (`sshCommandProvider`)
- Dropped [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin) from bundled plugins temporary because we couldn't complete update for Scalatra 2.6 before this release.
See the [change log](CHANGELOG.md) for all of the updates. See the [change log](CHANGELOG.md) for all of the updates.

208
build.sbt
View File

@@ -1,78 +1,68 @@
import com.typesafe.sbt.license.{DepModuleInfo, LicenseInfo} import com.typesafe.sbt.license.{LicenseInfo, DepModuleInfo}
import com.typesafe.sbt.pgp.PgpKeys._ import com.typesafe.sbt.pgp.PgpKeys._
val Organization = "io.github.gitbucket" val Organization = "io.github.gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "4.33.0" val GitBucketVersion = "4.19.3"
val ScalatraVersion = "2.7.0-RC1" val ScalatraVersion = "2.6.1"
val JettyVersion = "9.4.30.v20200611" val JettyVersion = "9.4.7.v20170914"
val JgitVersion = "5.8.0.202006091008-r"
lazy val root = (project in file(".")) lazy val root = (project in file(".")).enablePlugins(SbtTwirl, ScalatraPlugin, JRebelPlugin).settings(
.enablePlugins(SbtTwirl, ScalatraPlugin)
.settings( )
)
sourcesInBase := false sourcesInBase := false
organization := Organization organization := Organization
name := Name name := Name
version := GitBucketVersion version := GitBucketVersion
scalaVersion := "2.13.1" scalaVersion := "2.12.4"
scalafmtOnCompile := true
coverageExcludedPackages := ".*\\.html\\..*"
// dependency settings // dependency settings
resolvers ++= Seq( resolvers ++= Seq(
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
Resolver.jcenterRepo, Resolver.jcenterRepo,
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/" "amateras" at "http://amateras.sourceforge.jp/mvn/",
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/",
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
) )
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % JgitVersion, "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.9.0.201710071750-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % JgitVersion, "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.9.0.201710071750-r",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion, "org.scalatra" %% "scalatra-forms" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.6.9", "org.json4s" %% "json4s-jackson" % "3.5.1",
"commons-io" % "commons-io" % "2.7", "commons-io" % "commons-io" % "2.5",
"io.github.gitbucket" % "solidbase" % "1.0.3", "io.github.gitbucket" % "solidbase" % "1.0.2",
"io.github.gitbucket" % "markedj" % "1.0.16", "io.github.gitbucket" % "markedj" % "1.0.15",
"org.apache.commons" % "commons-compress" % "1.20", "org.apache.commons" % "commons-compress" % "1.13",
"org.apache.commons" % "commons-email" % "1.5", "org.apache.commons" % "commons-email" % "1.4",
"commons-net" % "commons-net" % "3.6", "org.apache.httpcomponents" % "httpclient" % "4.5.3",
"org.apache.httpcomponents" % "httpclient" % "4.5.12", "org.apache.sshd" % "apache-sshd" % "1.4.0" exclude("org.slf4j","slf4j-jdk14"),
"org.apache.sshd" % "apache-sshd" % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"), "org.apache.tika" % "tika-core" % "1.14",
"org.apache.tika" % "tika-core" % "1.24.1", "com.github.takezoe" %% "blocking-slick-32" % "0.0.10",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.4.195",
"com.h2database" % "h2" % "1.4.199", "org.mariadb.jdbc" % "mariadb-java-client" % "2.1.2",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.6.0", "org.postgresql" % "postgresql" % "42.0.0",
"org.postgresql" % "postgresql" % "42.2.6", "ch.qos.logback" % "logback-classic" % "1.2.3",
"ch.qos.logback" % "logback-classic" % "1.2.3", "com.zaxxer" % "HikariCP" % "2.6.1",
"com.zaxxer" % "HikariCP" % "3.4.5", "com.typesafe" % "config" % "1.3.1",
"com.typesafe" % "config" % "1.4.0", "com.typesafe.akka" %% "akka-actor" % "2.5.0",
"com.typesafe.akka" %% "akka-actor" % "2.5.27", "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0", "com.github.bkromhout" % "java-diff-utils" % "2.1.1",
"com.github.bkromhout" % "java-diff-utils" % "2.1.1", "org.cache2k" % "cache2k-all" % "1.0.0.CR1",
"org.cache2k" % "cache2k-all" % "1.2.4.Final", "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"),
"com.enragedginger" %% "akka-quartz-scheduler" % "1.8.1-akka-2.5.x" exclude ("com.mchange", "c3p0") exclude ("com.zaxxer", "HikariCP-java6"), "net.coobird" % "thumbnailator" % "0.4.8",
"net.coobird" % "thumbnailator" % "0.4.11", "com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.github.zafarkhaja" % "java-semver" % "0.9.0", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"com.nimbusds" % "oauth2-oidc-sdk" % "5.64.4", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "junit" % "junit" % "4.12" % "test",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"junit" % "junit" % "4.13" % "test", "org.mockito" % "mockito-core" % "2.7.22" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test", "com.wix" % "wix-embedded-mysql" % "2.1.4" % "test",
"org.mockito" % "mockito-core" % "3.3.3" % "test", "ru.yandex.qatools.embed" % "postgresql-embedded" % "2.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.37.0" % "test", "net.i2p.crypto" % "eddsa" % "0.1.0"
"org.testcontainers" % "mysql" % "1.14.3" % "test",
"org.testcontainers" % "postgresql" % "1.14.3" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.0.3"
) )
// Compiler settings // Compiler settings
@@ -83,7 +73,7 @@ javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
// Test settings // Test settings
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest") //testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test" javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
testOptions in Test += Tests.Setup(() => new java.io.File("target/gitbucket_home_for_test").mkdir()) testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() )
fork in Test := true fork in Test := true
// Packaging options // Packaging options
@@ -93,18 +83,25 @@ packageOptions += Package.MainClass("JettyLauncher")
test in assembly := {} test in assembly := {}
assemblyMergeStrategy in assembly := { assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) => case PathList("META-INF", xs @ _*) =>
(xs map { _.toLowerCase }) match { (xs map {_.toLowerCase}) match {
case ("manifest.mf" :: Nil) => MergeStrategy.discard case ("manifest.mf" :: Nil) => MergeStrategy.discard
case _ => MergeStrategy.discard case _ => MergeStrategy.discard
} }
case x => MergeStrategy.first case x => MergeStrategy.first
} }
// JRebel
//Seq(jrebelSettings: _*)
//jrebel.webLinks += (target in webappPrepare).value
//jrebel.enabled := System.getenv().get("JREBEL") != null
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
}
// Exclude a war file from published artifacts // Exclude a war file from published artifacts
signedArtifacts := { signedArtifacts := {
signedArtifacts.value.filterNot { signedArtifacts.value.filterNot { case (_, file) => file.getName.endsWith(".war") || file.getName.endsWith(".war.asc") }
case (_, file) => file.getName.endsWith(".war") || file.getName.endsWith(".war.asc")
}
} }
// Create executable war file // Create executable war file
@@ -124,34 +121,35 @@ libraryDependencies ++= Seq(
val executableKey = TaskKey[File]("executable") val executableKey = TaskKey[File]("executable")
executableKey := { executableKey := {
import java.util.jar.Attributes.{Name => AttrName} import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.{Manifest => JarManifest} import java.util.jar.Attributes.{ Name => AttrName }
val workDir = Keys.target.value / "executable" val workDir = Keys.target.value / "executable"
val warName = Keys.name.value + ".war" val warName = Keys.name.value + ".war"
val log = streams.value.log val log = streams.value.log
log info s"building executable webapp in ${workDir}" log info s"building executable webapp in ${workDir}"
// initialize temp directory // initialize temp directory
val temp = workDir / "webapp" val temp = workDir / "webapp"
IO delete temp IO delete temp
// include jetty classes // include jetty classes
val jettyJars = Keys.update.value select configurationFilter(name = ExecutableConfig.name) val jettyJars = Keys.update.value select configurationFilter(name = ExecutableConfig.name)
jettyJars foreach { jar => jettyJars foreach { jar =>
IO unzip (jar, temp, (name: String) => IO unzip (jar, temp, (name:String) =>
(name startsWith "javax/") || (name startsWith "javax/") ||
(name startsWith "org/")) (name startsWith "org/")
)
} }
// include original war file // include original war file
val warFile = (Keys.`package`).value val warFile = (Keys.`package`).value
IO unzip (warFile, temp) IO unzip (warFile, temp)
// include launcher classes // include launcher classes
val classDir = (Keys.classDirectory in Compile).value val classDir = (Keys.classDirectory in Compile).value
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */ ) val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
launchClasses foreach { name => launchClasses foreach { name =>
IO copyFile (classDir / name, temp / name) IO copyFile (classDir / name, temp / name)
} }
@@ -159,39 +157,33 @@ executableKey := {
// include plugins // include plugins
val pluginsDir = temp / "WEB-INF" / "classes" / "plugins" val pluginsDir = temp / "WEB-INF" / "classes" / "plugins"
IO createDirectory (pluginsDir) IO createDirectory (pluginsDir)
IO copyFile(Keys.baseDirectory.value / "plugins.json", pluginsDir / "plugins.json")
val plugins = IO readLines (Keys.baseDirectory.value / "src" / "main" / "resources" / "bundle-plugins.txt") val json = IO read(Keys.baseDirectory.value / "plugins.json")
plugins.foreach { plugin => PluginsJson.parse(json).foreach { case (plugin, version, file) =>
plugin.trim.split(":") match { val url = s"https://github.com/gitbucket/${plugin}/releases/download/${version}/${file}"
case Array(pluginId, pluginVersion) => log info s"Download: ${url}"
val url = "https://github.com/" + IO transfer(new java.net.URL(url).openStream, pluginsDir / file)
s"gitbucket/gitbucket-${pluginId}-plugin/releases/download/${pluginVersion}/gitbucket-${pluginId}-plugin-${pluginVersion}.jar"
log info s"Download: ${url}"
IO transfer (new java.net.URL(url).openStream, pluginsDir / url.substring(url.lastIndexOf("/") + 1))
case _ => ()
}
} }
// zip it up // zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF") IO delete (temp / "META-INF" / "MANIFEST.MF")
val contentMappings = (temp.allPaths --- PathFinder(temp)).get pair { file => val contentMappings = (temp.allPaths --- PathFinder(temp)).get pair { file => IO.relativizeFile(temp, file) }
IO.relativizeFile(temp, file) val manifest = new JarManifest
}
val manifest = new JarManifest
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName val outputFile = workDir / warName
IO jar (contentMappings.map { case (file, path) => (file, path.toString) }, outputFile, manifest) IO jar (contentMappings.map { case (file, path) => (file, path.toString) } , outputFile, manifest)
// generate checksums // generate checksums
Seq( Seq(
"md5" -> "MD5", "md5" -> "MD5",
"sha1" -> "SHA-1", "sha1" -> "SHA-1",
"sha256" -> "SHA-256" "sha256" -> "SHA-256"
).foreach { )
case (extension, algorithm) => .foreach { case (extension, algorithm) =>
val checksumFile = workDir / (warName + "." + extension) val checksumFile = workDir / (warName + "." + extension)
Checksums generate (outputFile, checksumFile, algorithm) Checksums generate (outputFile, checksumFile, algorithm)
} }
// done // done
@@ -201,12 +193,10 @@ executableKey := {
publishTo := { publishTo := {
val nexus = "https://oss.sonatype.org/" val nexus = "https://oss.sonatype.org/"
if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
else Some("releases" at nexus + "service/local/staging/deploy/maven2") else Some("releases" at nexus + "service/local/staging/deploy/maven2")
} }
publishMavenStyle := true publishMavenStyle := true
pomIncludeRepository := { _ => pomIncludeRepository := { _ => false }
false
}
pomExtra := ( pomExtra := (
<url>https://github.com/gitbucket/gitbucket</url> <url>https://github.com/gitbucket/gitbucket</url>
<licenses> <licenses>
@@ -257,17 +247,3 @@ licenseOverrides := {
case DepModuleInfo("com.github.bkromhout", "java-diff-utils", _) => case DepModuleInfo("com.github.bkromhout", "java-diff-utils", _) =>
LicenseInfo(LicenseCategory.Apache, "Apache-2.0", "http://www.apache.org/licenses/LICENSE-2.0") LicenseInfo(LicenseCategory.Apache, "Apache-2.0", "http://www.apache.org/licenses/LICENSE-2.0")
} }
testOptions in Test ++= {
if (scala.util.Properties.isWin) {
Seq(
Tests.Exclude(
Set(
"gitbucket.core.GitBucketCoreModuleSpec"
)
)
)
} else {
Nil
}
}

View File

@@ -1,21 +0,0 @@
module gitbucket 1.0;
require {
type smtp_port_t;
type tomcat_t;
type tomcat_var_lib_t;
type unreserved_port_t;
class file { execute };
class tcp_socket { name_bind };
class tcp_socket { name_connect };
}
# allow tomcat to send emails
allow tomcat_t smtp_port_t:tcp_socket { name_connect };
# allow file executes, required during repo creation
allow tomcat_t tomcat_var_lib_t:file { execute };
# allow tomcat to serve repositories via SSH
allow tomcat_t unreserved_port_t:tcp_socket { name_bind };

View File

@@ -1,32 +0,0 @@
# Red Hat Enterprise Linux / CentOS SELinux policy module for GitBucket
One way to run GitBucket on Enterprise Linux is under Tomcat. Since EL 7.4, Tomcat is no longer unconfined.
Thus since 7.4, Enterprise Linux blocks certain operations that are required for GitBucket to work properly:
* Tomcat is not allowed to connect to SMTP ports, which is required to send email notifications.
* Tomcat is not allowed to execute files, which is required for creating repositories.
* Tomcat is not allowed to act as a server on unreserved ports, which is required for serving repositories via SSH.
To mitigate this, you can use the SELinux policy module provided as `gitbucket.te`. You can deploy the module with the
attached script, e.g.:
~~~
./sedeploy.sh gitbucket
~~~
You most likely also need to fix file contexts on your system. Assuming a new, default Tomcat installation on 7.4, you
can do so by issuing the following commands:
~~~
GITBUCKET_HOME='/usr/share/tomcat/.gitbucket'
mkdir -p ${GITBUCKET_HOME}
chown tomcat.tomcat ${GITBUCKET_HOME}
semanage fcontext -a -t tomcat_var_lib_t "${GITBUCKET_HOME}(/.*)?"
restorecon -rv ${GITBUCKET_HOME}
JAVA_CONF='/usr/share/tomcat/.java'
mkdir -p ${JAVA_CONF}
chown tomcat.tomcat ${JAVA_CONF}
semanage fcontext -a -t tomcat_cache_t "${JAVA_CONF}(/.*)?"
restorecon -rv ${JAVA_CONF}
~~~

View File

@@ -1,14 +0,0 @@
#!/bin/sh
set -e
MODULE=${1}
# this will create a .mod file
checkmodule -M -m -o ${MODULE}.mod ${MODULE}.te
# this will create a compiled semodule
semodule_package -m ${MODULE}.mod -o ${MODULE}.pp
# this will install the module
semodule -i ${MODULE}.pp

View File

@@ -6,26 +6,17 @@ The details are saved at `ISSUE_COMMENT` table.
To determine if it was any operation, you see the `ACTION` column. To determine if it was any operation, you see the `ACTION` column.
And in the case of some actions, `CONTENT` column value contains additional information. And in the case of some actions, `CONTENT` column value contains additional information.
|ACTION |CONTENT | |ACTION |CONTENT |
|----------------|--------------------------| |---------------|-----------------|
|comment |comment | |comment |comment |
|close_comment |comment | |close_comment |comment |
|reopen_comment |comment | |reopen_comment |comment |
|close |"Close" | |close |"Close" |
|reopen |"Reopen" | |reopen |"Reopen" |
|commit |comment commitId | |commit |comment commitId |
|merge |comment | |merge |comment |
|delete_branch |branchName | |delete_branch |branchName |
|refer |issueId:title | |refer |issueId:title |
|add_label |labelName |
|delete_label |labelName |
|change_priority |oldPriority:priority |
|change_milestone|oldMilestone:milestone |
|assign |oldAssigned:assigned |
|change_title |oldTitle(CRLF)title \[1\] |
\[1\]: (CRLF) is "\r\n"
### comment ### comment
@@ -63,27 +54,3 @@ Therefore, this comment is not displayed, and not counted as a comment.
This value is saved when other issue or issue comment contains reference to the issue like `#issueId`. This value is saved when other issue or issue comment contains reference to the issue like `#issueId`.
At the same time, store id and title of the referrer issue as `id:title`. At the same time, store id and title of the referrer issue as `id:title`.
### add_label
This value is saved when users have added the label.
### delete_label
This value is saved when users have deleted the label.
### change_priority
This value is saved when users have changed the priority.
### change_milestone
This value is saved when users have changed the milestone.
### assign
This value is saved when users have assign issue/PR to user or remove the assign.
### change_title
This value is saved when users have changed the title.

View File

@@ -1,22 +0,0 @@
Debug GitBucket on IntelliJ
========
Add following configuration for allowing remote debugging to `buils.sbt`:
```scala
javaOptions in Jetty ++= Seq(
"-Xdebug",
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
)
```
Run GitBucket:
```shell
$ sbt ~jetty:start
```
In IntelliJ, create remote debug configuration as follows. Make sure port number is same as above configuration.
![Remote debug configuration on IntelliJ](remote_debug.png)
Then you can start debugging on IntelliJ!

View File

@@ -9,17 +9,16 @@ This directory has following structure:
* /repositories * /repositories
* /USER_NAME * /USER_NAME
* /REPO_NAME.git (substance of repository. GitServlet sees this directory) * /REPO_NAME.git (substance of repository. GitServlet sees this directory)
* /REPO_NAME.wiki.git (wiki repository)
* /REPO_NAME * /REPO_NAME
* /issues (files which are attached to issue) * /issues (files which are attached to issue)
* /lfs (LFS managed files) * /REPO_NAME.wiki.git (wiki repository)
* /data * /data
* /USER_NAME * /USER_NAME
* /files * /files
* avatar.xxx (image file of user avatar) * avatar.xxx (image file of user avatar)
* /plugins * /plugins
* plugin.jar * /PLUGIN_NAME
* /.installed (copied available plugins from the parent directory automatically) * plugin.js
* /tmp * /tmp
* /_upload * /_upload
* /SESSION_ID (removed at session timeout) * /SESSION_ID (removed at session timeout)

View File

@@ -1,9 +1,9 @@
How to build and run from the source tree How to run from the source tree
======== ========
First of all, Install [sbt](http://www.scala-sbt.org/index.html). Install [sbt](http://www.scala-sbt.org/index.html) at first.
```shell ```
$ brew install sbt $ brew install sbt
``` ```
@@ -12,7 +12,7 @@ Run for Development
If you want to test GitBucket, type the following command in the root directory of the source tree. If you want to test GitBucket, type the following command in the root directory of the source tree.
```shell ```
$ sbt ~jetty:start $ sbt ~jetty:start
``` ```
@@ -25,15 +25,15 @@ Build war file
To build war file, run the following command: To build war file, run the following command:
```shell ```
$ sbt package $ sbt package
``` ```
`gitbucket_2.13-x.x.x.war` is generated into `target/scala-2.13`. `gitbucket_2.12-x.x.x.war` is generated into `target/scala-2.12`.
To build an executable war file, run To build an executable war file, run
```shell ```
$ sbt executable $ sbt executable
``` ```
@@ -41,21 +41,8 @@ at the top of the source tree. It generates executable `gitbucket.war` into `tar
Run tests spec Run tests spec
--------- ---------
Before running tests, you need to install docker.
```shell
$ brew cask install docker # Install Docker
$ open /Applications/Docker.app # Start Docker
```
To run the full series of tests, run the following command: To run the full series of tests, run the following command:
```shell ```
$ sbt test $ sbt test
``` ```
If you don't have docker, you can skip docker tests which require docker as follows:
```shell
$ sbt "testOnly * -- -l ExternalDBTest"
```

120
doc/jrebel.md Normal file
View File

@@ -0,0 +1,120 @@
JRebel integration (optional)
=============================
[JRebel](https://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
JRebel is generally able to eliminate the need for the slow "app restart" per modification of codes. Alsp it's only used during development, and doesn't change your deployed app in any way.
JRebel is not open source, but we can use it free for non-commercial use.
----
## 1. Get a JRebel license
Sign up for a [myJRebel](https://my.jrebel.com/register). You will need to create an account.
## 2. Download JRebel
Download the most recent ["nosetup" JRebel zip](https://zeroturnaround.com/software/jrebel/download/prev-releases/).
Next, unzip the downloaded file.
## 3. Activate
Follow `readme.txt` in the extracted directory to activate your downloaded JRebel.
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
## 4. Tell jvm where JRebel is
Fortunately, the gitbucket project is already set up to use JRebel.
You only need to tell jvm where to find the jrebel jar.
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
```bash
export JREBEL=/path/to/jrebel/legacy/jrebel.jar
```
For example, if you unzipped your JRebel download in your home directory, you whould use:
```bash
export JREBEL=~/jrebel/legacy/jrebel.jar
```
Now reload your shell:
```
$ source ~/.bash_profile # on Mac
$ source ~/.bashrc # on Linux
```
## 5. See it in action!
Now you're ready to use JRebel with the gitbucket.
When you run sbt as normal, you will see a long message from JRebel, indicating it has loaded.
Here's an abbreviated version of what you will see:
```
$ ./sbt
[info] Loading project definition from /git/gitbucket/project
[info] Set current project to gitbucket (in build file:/git/gitbucket/)
>
```
You will start the servlet container slightly differently now that you're using sbt.
```
> jetty:quickstart
:
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: #############################################################
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Legacy Agent 7.0.15 (201709080836)
2017-09-21 15:46:35 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Over the last 2 days JRebel prevented
2017-09-21 15:46:35 JRebel: at least 8 redeploys/restarts saving you about 0.3 hours.
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Licensed to Naoki Takezoe (using myJRebel).
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: #############################################################
2017-09-21 15:46:35 JRebel:
:
> ~compile
[success] Total time: 2 s, completed 2017/09/21 15:50:06
1. Waiting for source changes... (press enter to interrupt)
```
Finally, change your code.
For example, you can change the title on `src/main/twirl/gitbucket/core/main.scala.html` like this:
```html
:
<a href="@context.path/" class="logo">
<img src="@helpers.assets("/common/images/gitbucket.svg")" style="width: 24px; height: 24px; display: inline;"/>
GitBucket
change code !!!!!!!!!!!!!!!!
<span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
</a>
:
```
If JRebel is doing is correctly installed you will see a notice for you:
```
1. Waiting for source changes... (press enter to interrupt)
[info] Compiling 1 Scala source to /Users/naoki.takezoe/gitbucket/target/scala-2.12/classes...
[success] Total time: 1 s, completed 2017/09/21 15:55:40
```
And you reload browser, JRebel give notice of that it has reloaded classes:
```
2. Waiting for source changes... (press enter to interrupt)
2017-09-21 15:55:40 JRebel: Reloading class 'gitbucket.core.html.main$'.
```
## 6. Limitations
JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routing patterns, there is nothing JRebel can do, you will have to restart by `jetty:quickstart`.

View File

@@ -4,128 +4,98 @@ Category | License | Dependency | Notes
--- | --- | --- | --- --- | --- | --- | ---
Apache | [ Apache License, Version 2.0 ]( http://opensource.org/licenses/apache2.0.php ) | org.osgi # org.osgi.core # 4.3.1 | <notextile></notextile> Apache | [ Apache License, Version 2.0 ]( http://opensource.org/licenses/apache2.0.php ) | org.osgi # org.osgi.core # 4.3.1 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.googlecode.javaewah # JavaEWAH # 1.1.6 | <notextile></notextile> Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.googlecode.javaewah # JavaEWAH # 1.1.6 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0) | org.cache2k # cache2k-all # 1.2.0.Final | <notextile></notextile> Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0) | org.cache2k # cache2k-all # 1.0.0.CR1 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0) | org.cache2k # cache2k-api # 1.2.0.Final | <notextile></notextile> Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.objenesis # objenesis # 2.5 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0) | org.cache2k # cache2k-core # 1.2.0.Final | <notextile></notextile> Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # apache-sshd # 1.4.0 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.objenesis # objenesis # 2.6 | <notextile></notextile> Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-core # 1.4.0 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # apache-sshd # 2.1.0 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.typesafe # config # 1.3.1 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-cli # 2.1.0 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.typesafe.akka # akka-actor_2.12 # 2.5.0 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-common # 2.1.0 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | commons-io # commons-io # 2.5 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-core # 2.1.0 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | fr.brouillard.oss.security.xhub # xhub4j-core # 1.0.0 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-putty # 2.1.0 | <notextile></notextile> Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-compress # 1.13 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-scp # 2.1.0 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-email # 1.4 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-sftp # 2.1.0 | <notextile></notextile> Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-lang3 # 3.5 | <notextile></notextile>
Apache | [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.java.dev.jna # jna # 4.5.1 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpclient # 4.5.3 | <notextile></notextile>
Apache | [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.java.dev.jna # jna-platform # 4.5.1 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpcore # 4.4.6 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.github.stephenc.jcip # jcip-annotations # 1.0-1 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpmime # 4.5.2 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.kohlschutter.junixsocket # junixsocket-common # 2.0.4 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.tika # tika-core # 1.14 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.kohlschutter.junixsocket # junixsocket-native-common # 2.0.4 | <notextile></notextile> Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.liquibase # liquibase-core # 3.4.1 | <notextile></notextile>
Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) | com.typesafe # config # 1.3.3 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-http # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.typesafe.akka # akka-actor_2.12 # 2.5.18 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-io # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) | commons-io # commons-io # 2.6 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-security # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | is.tagomor.woothee # woothee-java # 1.8.0 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-server # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-compress # 1.18 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-servlet # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-email # 1.5 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-util # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-lang3 # 3.6 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-webapp # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpclient # 4.5.6 | <notextile></notextile> Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-xml # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpcore # 4.4.10 | <notextile></notextile> Apache | [Apache Software License, Version 1.1](http://www.apache.org/licenses/LICENSE-1.1) | org.bouncycastle # bcpg-jdk15on # 1.56 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpmime # 4.5.3 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.tika # tika-core # 1.19.1 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.ec4j.core # ec4j-core # 0.0.3 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.liquibase # liquibase-core # 3.6.2 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.yaml # snakeyaml # 1.18 | <notextile></notextile>
Apache | [Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) | com.nimbusds # oauth2-oidc-sdk # 5.64.4 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-http # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-io # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-security # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-server # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-servlet # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-util # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-webapp # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-xml # 9.4.6.v20170531 | <notextile></notextile>
Apache | [Apache Software License, Version 1.1](http://www.apache.org/licenses/LICENSE-1.1) | org.bouncycastle # bcpg-jdk15on # 1.60 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.github.bkromhout # java-diff-utils # 2.1.1 | <notextile></notextile> Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.github.bkromhout # java-diff-utils # 2.1.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.html) | com.typesafe.play # twirl-api_2.12 # 1.3.15 | <notextile></notextile> Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.html) | com.typesafe.play # twirl-api_2.12 # 1.3.7 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-ast_2.12 # 3.5.2 | <notextile></notextile> Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-ast_2.12 # 3.5.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-core_2.12 # 3.5.2 | <notextile></notextile> Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-core_2.12 # 3.5.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-jackson_2.12 # 3.5.2 | <notextile></notextile> Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-jackson_2.12 # 3.5.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-scalap_2.12 # 3.5.2 | <notextile></notextile> Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-scalap_2.12 # 3.5.1 | <notextile></notextile>
Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang # scala-library # 2.12.8 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.enragedginger # akka-quartz-scheduler_2.12 # 1.6.0-akka-2.4.x | <notextile></notextile>
Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang # scala-reflect # 2.12.8 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.enragedginger # akka-quartz-scheduler_2.12 # 1.7.0-akka-2.5.x | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-annotations # 2.8.0 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-annotations # 2.8.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-core # 2.8.4 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-core # 2.8.4 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-databind # 2.8.4 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-databind # 2.8.4 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.github.takezoe # blocking-slick-32_2.12 # 0.0.11 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.github.takezoe # blocking-slick-32_2.12 # 0.0.10 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.google.code.findbugs # jsr305 # 3.0.0 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.google.code.findbugs # jsr305 # 3.0.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.nimbusds # lang-tag # 1.4.3 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.zaxxer # HikariCP # 2.6.1 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.nimbusds # nimbus-jose-jwt # 5.5 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | commons-codec # commons-codec # 1.9 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.zaxxer # HikariCP # 3.2.0 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | commons-logging # commons-logging # 1.2 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | de.flapdoodle.embed # de.flapdoodle.embed.process # 2.0.1 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | eu.medsea.mimeutil # mime-util # 2.1.3 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | eu.medsea.mimeutil # mime-util # 2.1.3 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # markedj # 1.0.15 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # markedj # 1.0.15 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # solidbase # 1.0.3 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # scalatra-forms_2.12 # 1.1.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.bytebuddy # byte-buddy # 1.9.3 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # solidbase # 1.0.2 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.bytebuddy # byte-buddy-agent # 1.9.3 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.bytebuddy # byte-buddy # 1.6.11 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.minidev # accessors-smart # 1.2 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.bytebuddy # byte-buddy-agent # 1.6.11 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.minidev # json-smart # 2.3 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.quartz-scheduler # quartz # 2.2.3 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.jetbrains # annotations # 15.0 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | ru.yandex.qatools.embed # postgresql-embedded # 2.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.quartz-scheduler # quartz # 2.3.0 | <notextile></notextile> Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | tomcat # tomcat-apr # 5.5.23 | <notextile></notextile>
Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalactic # scalactic_2.12 # 3.0.3 | <notextile></notextile> Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalactic # scalactic_2.12 # 3.0.0 | <notextile></notextile>
Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest_2.12 # 3.0.3 | <notextile></notextile> Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest_2.12 # 3.0.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-common_2.12 # 2.6.3 | <notextile></notextile> BSD | [BSD](LICENSE.txt) | com.thoughtworks.paranamer # paranamer # 2.8 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-forms_2.12 # 2.6.3 | <notextile></notextile> BSD | [BSD](http://software.clapper.org/grizzled-slf4j/license.html) | org.clapper # grizzled-slf4j_2.12 # 1.3.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-json_2.12 # 2.6.3 | <notextile></notextile> BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-common_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-scalatest_2.12 # 2.6.3 | <notextile></notextile> BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-json_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-test_2.12 # 2.6.3 | <notextile></notextile> BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-scalatest_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra_2.12 # 2.6.3 | <notextile></notextile> BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-test_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD 3-Clause](http://www.scala-lang.org/license.html) | org.scala-lang # scala-library # 2.12.3 | <notextile></notextile>
BSD | [BSD 3-Clause](http://www.scala-lang.org/license.html) | org.scala-lang # scala-reflect # 2.12.3 | <notextile></notextile>
BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-java8-compat_2.12 # 0.8.0 | <notextile></notextile> BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-java8-compat_2.12 # 0.8.0 | <notextile></notextile>
BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-parser-combinators_2.12 # 1.0.6 | <notextile></notextile> BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-parser-combinators_2.12 # 1.0.4 | <notextile></notextile>
BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-xml_2.12 # 1.0.6 | <notextile></notextile> BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-xml_2.12 # 1.0.6 | <notextile></notextile>
BSD | [BSD-2-Clause](https://jdbc.postgresql.org/about/license.html) | org.postgresql # postgresql # 42.2.5 | <notextile></notextile> BSD | [BSD License](http://www.opensource.org/licenses/bsd-license.php) | com.wix # wix-embedded-mysql # 2.1.4 | <notextile></notextile>
BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit # 5.2.0.201812061821-r | <notextile></notextile> BSD | [BSD-2-Clause](https://jdbc.postgresql.org/about/license.html) | org.postgresql # postgresql # 42.0.0 | <notextile></notextile>
BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit.archive # 5.2.0.201812061821-r | <notextile></notextile> BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit # 4.8.0.201706111038-r | <notextile></notextile>
BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit.http.server # 5.2.0.201812061821-r | <notextile></notextile> BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit.archive # 4.8.0.201706111038-r | <notextile></notextile>
BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit.http.server # 4.8.0.201706111038-r | <notextile></notextile>
BSD | [New BSD License](http://www.opensource.org/licenses/bsd-license.php) | org.hamcrest # hamcrest-core # 1.3 | <notextile></notextile> BSD | [New BSD License](http://www.opensource.org/licenses/bsd-license.php) | org.hamcrest # hamcrest-core # 1.3 | <notextile></notextile>
BSD | [Revised BSD](http://www.jcraft.com/jsch/LICENSE.txt) | com.jcraft # jsch # 0.1.54 | <notextile></notextile> BSD | [Revised BSD](http://www.jcraft.com/jsch/LICENSE.txt) | com.jcraft # jsch # 0.1.54 | <notextile></notextile>
BSD | [Revised BSD](http://www.jcraft.com/jzlib/LICENSE.txt) | com.jcraft # jzlib # 1.1.1 | <notextile></notextile>
BSD | [Two-clause BSD-style license](http://github.com/slick/slick/blob/master/LICENSE.txt) | com.typesafe.slick # slick_2.12 # 3.2.1 | <notextile></notextile> BSD | [Two-clause BSD-style license](http://github.com/slick/slick/blob/master/LICENSE.txt) | com.typesafe.slick # slick_2.12 # 3.2.1 | <notextile></notextile>
CC0 | [CC0](http://creativecommons.org/publicdomain/zero/1.0/) | org.reactivestreams # reactive-streams # 1.0.0 | <notextile></notextile> CC0 | [CC0](http://creativecommons.org/publicdomain/zero/1.0/) | org.reactivestreams # reactive-streams # 1.0.0 | <notextile></notextile>
CC0 | [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/) | net.i2p.crypto # eddsa # 0.3.0 | <notextile></notextile> CDDL | [COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0](https://glassfish.dev.java.net/public/CDDLv1.0.html) | javax.activation # activation # 1.1.1 | <notextile></notextile>
CC0 | [CC0 1.0 Universal License](http://creativecommons.org/publicdomain/zero/1.0/) | org.scijava # native-lib-loader # 2.0.2 | <notextile></notextile> GPL | [CDDL/GPLv2+CE](https://glassfish.java.net/public/CDDL+GPL_1_1.html) | com.sun.mail # javax.mail # 1.5.2 | <notextile></notextile>
CDDL | [Common Development and Distribution License (CDDL) v1.0](https://glassfish.dev.java.net/public/CDDLv1.0.html) | javax.activation # activation # 1.1 | <notextile></notextile>
GPL | [CDDL/GPLv2+CE](https://javaee.github.io/javamail/LICENSE) | com.sun.mail # javax.mail # 1.6.1 | <notextile></notextile>
GPL | [GPL2 w/ CPE](https://oss.oracle.com/licenses/CDDL+GPL-1.1) | javax.xml.bind # jaxb-api # 2.3.0 | <notextile></notextile>
GPL with Classpath Extension | [CDDL + GPLv2 with classpath exception](https://github.com/javaee/javax.annotation/blob/master/LICENSE) | javax.annotation # javax.annotation-api # 1.3.1 | <notextile></notextile>
GPL with Classpath Extension | [CDDL + GPLv2 with classpath exception](https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html) | javax.servlet # javax.servlet-api # 3.1.0 | <notextile></notextile> GPL with Classpath Extension | [CDDL + GPLv2 with classpath exception](https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html) | javax.servlet # javax.servlet-api # 3.1.0 | <notextile></notextile>
LGPL | [GNU Lesser General Public License](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) | ch.qos.logback # logback-classic # 1.2.3 | <notextile></notextile> LGPL | [GNU Lesser General Public License](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) | ch.qos.logback # logback-classic # 1.2.3 | <notextile></notextile>
LGPL | [GNU Lesser General Public License](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) | ch.qos.logback # logback-core # 1.2.3 | <notextile></notextile> LGPL | [GNU Lesser General Public License](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) | ch.qos.logback # logback-core # 1.2.3 | <notextile></notextile>
LGPL | [GNU Lesser General Public License, Version 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) | com.mchange # c3p0 # 0.9.5.2 | <notextile></notextile> LGPL | [LGPL, version 2.1](http://www.gnu.org/licenses/licenses.html) | net.java.dev.jna # jna # 4.0.0 | <notextile></notextile>
LGPL | [GNU Lesser General Public License, Version 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) | com.mchange # mchange-commons-java # 0.2.11 | <notextile></notextile> LGPL | [LGPL, version 2.1](http://www.gnu.org/licenses/licenses.html) | net.java.dev.jna # jna-platform # 4.0.0 | <notextile></notextile>
LGPL | [LGPL-2.1](null) | org.mariadb.jdbc # mariadb-java-client # 2.3.0 | <notextile></notextile> LGPL | [LGPL-2.1](null) | org.mariadb.jdbc # mariadb-java-client # 2.0.3 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.rnorth # tcp-unix-socket-proxy # 1.0.2 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.rnorth.duct-tape # duct-tape # 1.0.7 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.rnorth.visible-assertions # visible-assertions # 2.1.1 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.testcontainers # database-commons # 1.10.3 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.testcontainers # jdbc # 1.10.3 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.testcontainers # mysql # 1.10.3 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.testcontainers # postgresql # 1.10.3 | <notextile></notextile>
MIT | [MIT](http://opensource.org/licenses/MIT) | org.testcontainers # testcontainers # 1.10.3 | <notextile></notextile>
MIT | [MIT License](http://www.opensource.org/licenses/mit-license.php) | org.slf4j # slf4j-api # 1.7.25 | <notextile></notextile> MIT | [MIT License](http://www.opensource.org/licenses/mit-license.php) | org.slf4j # slf4j-api # 1.7.25 | <notextile></notextile>
MIT | [The MIT License](http://www.opensource.org/licenses/mit-license.php) | com.github.zafarkhaja # java-semver # 0.9.0 | <notextile></notextile> MIT | [The MIT License](http://www.opensource.org/licenses/mit-license.php) | com.github.zafarkhaja # java-semver # 0.9.0 | <notextile></notextile>
MIT | [The MIT License](https://jsoup.org/license) | org.jsoup # jsoup # 1.10.2 | <notextile></notextile> MIT | [The MIT License](https://jsoup.org/license) | org.jsoup # jsoup # 1.10.2 | <notextile></notextile>
MIT | [The MIT License](https://github.com/mockito/mockito/blob/master/LICENSE) | org.mockito # mockito-core # 2.23.4 | <notextile></notextile> MIT | [The MIT License](http://github.com/mockito/mockito/blob/master/LICENSE) | org.mockito # mockito-all # 1.10.19 | <notextile></notextile>
MIT | [The MIT License (MIT)](https://opensource.org/licenses/MIT) | com.dimafeng # testcontainers-scala_2.12 # 0.22.0 | <notextile></notextile> MIT | [The MIT License](http://github.com/mockito/mockito/blob/master/LICENSE) | org.mockito # mockito-core # 2.7.22 | <notextile></notextile>
MIT | [The MIT License (MIT)](http://www.opensource.org/licenses/mit-license.html) | net.coobird # thumbnailator # 0.4.8 | <notextile></notextile> MIT | [The MIT License (MIT)](http://www.opensource.org/licenses/mit-license.html) | net.coobird # thumbnailator # 0.4.8 | <notextile></notextile>
Mozilla | [MPL 2.0 or EPL 1.0](http://h2database.com/html/license.html) | com.h2database # h2 # 1.4.197 | <notextile></notextile> Mozilla | [MPL 2.0 or EPL 1.0](http://h2database.com/html/license.html) | com.h2database # h2 # 1.4.195 | <notextile></notextile>
Mozilla | [Mozilla Public License 1.1 (MPL 1.1)](http://www.mozilla.org/MPL/MPL-1.1.html) | com.googlecode.juniversalchardet # juniversalchardet # 1.0.3 | <notextile></notextile> Mozilla | [Mozilla Public License 1.1 (MPL 1.1)](http://www.mozilla.org/MPL/MPL-1.1.html) | com.googlecode.juniversalchardet # juniversalchardet # 1.0.3 | <notextile></notextile>
unrecognized | [Bouncy Castle Licence](http://www.bouncycastle.org/licence.html) | org.bouncycastle # bcpkix-jdk15on # 1.60 | <notextile></notextile> Public Domain | [Public Domain](http://en.wikipedia.org/wiki/Public_domain) | net.i2p.crypto # eddsa # 0.1.0 | <notextile></notextile>
unrecognized | [Bouncy Castle Licence](http://www.bouncycastle.org/licence.html) | org.bouncycastle # bcprov-jdk15on # 1.60 | <notextile></notextile> unrecognized | [Bouncy Castle Licence](http://www.bouncycastle.org/licence.html) | org.bouncycastle # bcpkix-jdk15on # 1.56 | <notextile></notextile>
unrecognized | [Bouncy Castle Licence](http://www.bouncycastle.org/licence.html) | org.bouncycastle # bcprov-jdk15on # 1.56 | <notextile></notextile>
unrecognized | [Eclipse Public License 1.0](http://www.eclipse.org/legal/epl-v10.html) | junit # junit # 4.12 | <notextile></notextile> unrecognized | [Eclipse Public License 1.0](http://www.eclipse.org/legal/epl-v10.html) | junit # junit # 4.12 | <notextile></notextile>
unrecognized | [The OpenLDAP Public License](http://www.openldap.org/software/release/license.html) | com.novell.ldap # jldap # 2009-10-07 | <notextile></notextile> unrecognized | [The OpenLDAP Public License](http://www.openldap.org/software/release/license.html) | com.novell.ldap # jldap # 2009-10-07 | <notextile></notextile>
unrecognized | [none specified](none specified) | com.thoughtworks.paranamer # paranamer # 2.8 | <notextile></notextile>
unrecognized | [none specified](none specified) | commons-codec # commons-codec # 1.10 | <notextile></notextile>
unrecognized | [none specified](none specified) | commons-logging # commons-logging # 1.2 | <notextile></notextile>
unrecognized | [none specified](none specified) | fr.brouillard.oss.security.xhub # xhub4j-core # 1.0.0 | <notextile></notextile>
unrecognized | [none specified](none specified) | org.ow2.asm # asm # 5.0.4 | <notextile></notextile>
unrecognized | [none specified](none specified) | tomcat # tomcat-apr # 5.5.23 | <notextile></notextile>

View File

@@ -1,7 +1,6 @@
Developer's Guide Developer's Guide
======== ========
* [Build from source tree](build.md) * [How to run from source tree](how_to_run.md)
* [Debug on IntelliJ](debug.md)
* [Directory Structure](directory.md) * [Directory Structure](directory.md)
* [Mapping and Validation](validation.md) * [Mapping and Validation](validation.md)
* [Authentication in Controller](authenticator.md) * [Authentication in Controller](authenticator.md)
@@ -9,4 +8,5 @@ Developer's Guide
* [Activity Types](activity.md) * [Activity Types](activity.md)
* [Automatic Schema Updating](auto_update.md) * [Automatic Schema Updating](auto_update.md)
* [Release Operation](release.md) * [Release Operation](release.md)
* [JRebel integration (optional)](jrebel.md)
* [Licenses](licenses.md) * [Licenses](licenses.md)

View File

@@ -34,20 +34,6 @@ object GitBucketCoreModule extends Module("gitbucket-core",
Generate release files Generate release files
-------- --------
### Deploy assembly jar file
For plug-in development, we have to publish the GitBucket jar file to the Maven central repository before release GitBucket itself.
First, hit following command to publish artifacts to the sonatype OSS repository:
```bash
$ sbt publishSigned
```
Then logged-in to https://oss.sonatype.org/, close and release the repository.
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 ### Make release war file
Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`. Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`.
@@ -56,4 +42,20 @@ Run `sbt executable`. The release war file and fingerprint are generated into `t
$ sbt executable $ sbt executable
``` ```
Create new release from the corresponded tag on GitHub, then upload generated jar file and fingerprints to the release. ### Deploy assembly jar file
For plug-in development, we have to publish the GitBucket jar file to the Maven central repository as well. At first, hit following command to publish artifacts to the sonatype OSS repository:
```bash
$ sbt publish-signed
```
Then logged-in https://oss.sonatype.org/ and delete following files from the staging repository:
- gitbucket_2.12-x.x.x.war
- gitbucket_2.12-x.x.x.war.asc
- gitbucket_2.12-x.x.x.war.asc.md5
- gitbucket_2.12-x.x.x.war.asc.sha1
- gitbucket_2.12-x.x.x.war.md5
At last, close and release the repository.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

View File

@@ -1,11 +1,11 @@
Mapping and Validation Mapping and 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. GitBucket uses [scalatra-forms](https://github.com/takezoe/scalatra-forms) to validate request parameters and map them to the scala object. This is inspired by Play2 form mapping / validation.
At first, define the mapping as following: At first, define the mapping as following:
```scala ```scala
import org.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
case class RegisterForm(name: String, description: String) case class RegisterForm(name: String, description: String)
@@ -15,17 +15,17 @@ val form = mapping(
)(RegisterForm.apply) )(RegisterForm.apply)
``` ```
The servlet have to mixed in `gitbucket.core.controller.ValidationFormSupport` to validate request parameters and take mapped object. It validates request parameters before action. If any errors are detected, it throws an exception. The servlet have to mixed in ```jp.sf.amateras.scalatra.forms.ClientSideValidationFormSupport``` to validate request parameters and take mapped object. It validates request parameters before action. If any errors are detected, it throws an exception.
```scala ```scala
class RegisterServlet extends ScalatraServlet with ValidationFormSupport { class RegisterServlet extends ScalatraServlet with ClientSideValidationFormSupport {
post("/register", form) { form: RegisterForm => post("/register", form) { form: RegisterForm =>
... ...
} }
} }
``` ```
In the view template, you can add client-side validation by adding `validate="true"` to your form. Error messages are set to `span#error-<fieldname>`. In the view template, you can add client-side validation by adding ```validate="true"``` to your form. Error messages are set to ```span#error-<fieldname>```.
```html ```html
<form method="POST" action="/register" validate="true"> <form method="POST" action="/register" validate="true">
@@ -39,9 +39,9 @@ In the view template, you can add client-side validation by adding `validate="tr
</form> </form>
``` ```
Client-side validation calls `<form-action>/validate` to validate form contents. It returns a validation result as JSON. In this case, form action is `/register`, so `/register/validate` is called before submitting a form. `ValidationFormSupport` adds this JSON API automatically. Client-side validation calls ```<form-action>/validate``` to validate form contents. It returns a validation result as JSON. In this case, form action is ```/register```, so ```/register/validate``` is called before submitting a form. ```ClientSideValidationFormSupport``` adds this JSON API automatically.
For Ajax request, you have to use `ajaxGet` or `ajaxPost` to define action. It almost same as '''get''' or '''post'''. You can implement actions which handle Ajax request as same as normal actions. For Ajax request, you have to use '''ajaxGet''' or '''ajaxPost''' to define action. It almost same as '''get''' or '''post'''. You can implement actions which handle Ajax request as same as normal actions.
Small difference is they return validation errors as JSON. Small difference is they return validation errors as JSON.
```scala ```scala

41
plugins.json Normal file
View File

@@ -0,0 +1,41 @@
[
{
"id": "notifications",
"name": "Notifications Plugin",
"description": "Provides notifications feature on GitBucket.",
"versions": [
{
"version": "1.4.0",
"range": ">=4.19.0",
"file": "gitbucket-notifications-plugin_2.12-1.4.0.jar"
}
],
"default": true
},
{
"id": "emoji",
"name": "Emoji Plugin",
"description": "Provides Emoji support for GitBucket.",
"versions": [
{
"version": "4.5.0",
"range": ">=4.18.0",
"file": "gitbucket-emoji-plugin_2.12-4.5.0.jar"
}
],
"default": false
},
{
"id": "gist",
"name": "Gist Plugin",
"description": "Provides Gist feature on GitBucket.",
"versions": [
{
"version": "4.11.0",
"range": ">=4.19.0",
"file": "gitbucket-gist-plugin-assembly-4.11.0.jar"
}
],
"default": false
}
]

View File

@@ -6,31 +6,29 @@ import io._
object Checksums { object Checksums {
private val bufferSize = 2048 private val bufferSize = 2048
def generate(source: File, target: File, algorithm: String): Unit = def generate(source:File, target:File, algorithm:String):Unit =
sbt.IO write (target, compute(source, algorithm)) sbt.IO write (target, compute(source, algorithm))
def compute(file: File, algorithm: String): String = def compute(file:File, algorithm:String):String =
hex(raw(file, algorithm)) hex(raw(file, algorithm))
def raw(file: File, algorithm: String): Array[Byte] = def raw(file:File, algorithm:String):Array[Byte] =
(Using fileInputStream file) { is => (Using fileInputStream file) { is =>
val md = MessageDigest getInstance algorithm val md = MessageDigest getInstance algorithm
val buf = new Array[Byte](bufferSize) val buf = new Array[Byte](bufferSize)
md.reset() md.reset()
@tailrec @tailrec
def loop(): Unit = { def loop() {
val len = is read buf val len = is read buf
if (len != -1) { if (len != -1) {
md update (buf, 0, len) md update (buf, 0, len)
loop() loop()
}
} }
loop()
md.digest()
} }
loop()
md.digest()
}
def hex(bytes: Array[Byte]): String = def hex(bytes:Array[Byte]):String =
bytes map { it => bytes map { it => "%02x" format (it.toInt & 0xff) } mkString ""
"%02x" format (it.toInt & 0xff)
} mkString ""
} }

View File

@@ -3,13 +3,19 @@ import scala.collection.JavaConverters._
object PluginsJson { object PluginsJson {
def getUrls(json: String): Seq[String] = { def parse(json: String): Seq[(String, String, String)] = {
val value = Json.parse(json) val value = Json.parse(json)
value.asArray.values.asScala.map { plugin => value.asArray.values.asScala.map { plugin =>
val pluginObject = plugin.asObject val pluginObject = plugin.asObject
val pluginName = "gitbucket-" + pluginObject.get("id").asString + "-plugin"
val latestVersionObject = pluginObject.get("versions").asArray.asScala.head.asObject val latestVersionObject = pluginObject.get("versions").asArray.asScala.head.asObject
latestVersionObject.get("url").asString val file = latestVersionObject.get("file").asString
val version = latestVersionObject.get("version").asString
(pluginName, version, file)
} }
} }
} }

View File

@@ -1 +1 @@
sbt.version=1.3.12 sbt.version=1.0.4

View File

@@ -1 +1 @@
libraryDependencies += "com.eclipsesource.minimal-json" % "minimal-json" % "0.9.5" libraryDependencies += "com.eclipsesource.minimal-json" % "minimal-json" % "0.9.4"

View File

@@ -1,10 +1,10 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.12")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") //addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4") //addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.1")
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2") addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC13")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")

View File

@@ -0,0 +1 @@
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC11")

View File

@@ -4,10 +4,6 @@ import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.server.session.DefaultSessionCache;
import org.eclipse.jetty.server.session.FileSessionDataStore;
import org.eclipse.jetty.server.session.SessionCache;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File; import java.io.File;
@@ -20,22 +16,13 @@ public class JettyLauncher {
System.setProperty("java.awt.headless", "true"); System.setProperty("java.awt.headless", "true");
String host = null; String host = null;
String port = null; int port = 8080;
InetSocketAddress address = null; InetSocketAddress address = null;
String contextPath = "/"; String contextPath = "/";
String tmpDirPath=""; String tmpDirPath="";
boolean forceHttps = false; boolean forceHttps = false;
boolean saveSessions = false;
host = getEnvironmentVariable("gitbucket.host");
port = getEnvironmentVariable("gitbucket.port");
contextPath = getEnvironmentVariable("gitbucket.prefix");
tmpDirPath = getEnvironmentVariable("gitbucket.tempDir");
for(String arg: args) { for(String arg: args) {
if(arg.equals("--save_sessions")) {
saveSessions = true;
}
if(arg.startsWith("--") && arg.contains("=")) { if(arg.startsWith("--") && arg.contains("=")) {
String[] dim = arg.split("="); String[] dim = arg.split("=");
if(dim.length >= 2) { if(dim.length >= 2) {
@@ -44,10 +31,16 @@ public class JettyLauncher {
host = dim[1]; host = dim[1];
break; break;
case "--port": case "--port":
port = dim[1]; port = Integer.parseInt(dim[1]);
break; break;
case "--prefix": case "--prefix":
contextPath = dim[1]; contextPath = dim[1];
if (!contextPath.startsWith("/")) {
contextPath = "/" + contextPath;
}
break;
case "--max_file_size":
System.setProperty("gitbucket.maxFileSize", dim[1]);
break; break;
case "--gitbucket.home": case "--gitbucket.home":
System.setProperty("gitbucket.home", dim[1]); System.setProperty("gitbucket.home", dim[1]);
@@ -58,19 +51,18 @@ public class JettyLauncher {
case "--plugin_dir": case "--plugin_dir":
System.setProperty("gitbucket.pluginDir", dim[1]); System.setProperty("gitbucket.pluginDir", dim[1]);
break; break;
case "--validate_password":
System.setProperty("gitbucket.validate.password", dim[1]);
break;
} }
} }
} }
} }
if (contextPath != null && !contextPath.startsWith("/")) {
contextPath = "/" + contextPath;
}
if(host != null) { if(host != null) {
address = new InetSocketAddress(host, getPort(port)); address = new InetSocketAddress(host, port);
} else { } else {
address = new InetSocketAddress(getPort(port)); address = new InetSocketAddress(port);
} }
Server server = new Server(address); Server server = new Server(address);
@@ -95,21 +87,8 @@ public class JettyLauncher {
WebAppContext context = new WebAppContext(); WebAppContext context = new WebAppContext();
if(saveSessions) {
File sessDir = new File(getGitBucketHome(), "sessions");
if(!sessDir.exists()){
sessDir.mkdirs();
}
SessionHandler sessions = context.getSessionHandler();
SessionCache cache = new DefaultSessionCache(sessions);
FileSessionDataStore fsds = new FileSessionDataStore();
fsds.setStoreDir(sessDir);
cache.setSessionDataStore(fsds);
sessions.setSessionCache(cache);
}
File tmpDir; File tmpDir;
if(tmpDirPath == null || tmpDirPath.equals("")){ if(tmpDirPath.equals("")){
tmpDir = new File(getGitBucketHome(), "tmp"); tmpDir = new File(getGitBucketHome(), "tmp");
if(!tmpDir.exists()){ if(!tmpDir.exists()){
tmpDir.mkdirs(); tmpDir.mkdirs();
@@ -132,7 +111,7 @@ public class JettyLauncher {
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain(); ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation(); URL location = domain.getCodeSource().getLocation();
context.setContextPath(contextPath == null ? "" : contextPath); context.setContextPath(contextPath);
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml"); context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server); context.setServer(server);
context.setWar(location.toExternalForm()); context.setWar(location.toExternalForm());
@@ -161,23 +140,6 @@ public class JettyLauncher {
return new File(System.getProperty("user.home"), ".gitbucket"); return new File(System.getProperty("user.home"), ".gitbucket");
} }
private static String getEnvironmentVariable(String key){
String value = System.getenv(key.toUpperCase().replace('.', '_'));
if (value != null && value.length() == 0){
return null;
} else {
return value;
}
}
private static int getPort(String port){
if(port == null) {
return 8080;
} else {
return Integer.parseInt(port);
}
}
private static Handler addStatisticsHandler(Handler handler) { private static Handler addStatisticsHandler(Handler handler) {
// The graceful shutdown is implemented via the statistics handler. // The graceful shutdown is implemented via the statistics handler.
// See the following: https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142 // See the following: https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142

View File

@@ -1,4 +0,0 @@
notifications:1.8.0
gist:4.18.0
emoji:4.6.0
pages:1.8.0

View File

@@ -60,12 +60,13 @@
<!-- ACCESS_TOKEN --> <!-- ACCESS_TOKEN -->
<!--================================================================================================--> <!--================================================================================================-->
<createTable tableName="ACCESS_TOKEN"> <createTable tableName="ACCESS_TOKEN">
<column name="ACCESS_TOKEN_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_ACCESS_TOKEN_PK" primaryKey="true" /> <column name="ACCESS_TOKEN_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="TOKEN_HASH" type="varchar(40)" nullable="false"/> <column name="TOKEN_HASH" type="varchar(40)" nullable="false"/>
<column name="USER_NAME" type="varchar(100)" nullable="false"/> <column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="NOTE" type="text" nullable="false"/> <column name="NOTE" type="text" nullable="false"/>
</createTable> </createTable>
<addPrimaryKey constraintName="IDX_ACCESS_TOKEN_PK" tableName="ACCESS_TOKEN" columnNames="ACCESS_TOKEN_ID"/>
<addUniqueConstraint constraintName="IDX_ACCESS_TOKEN_TOKEN_HASH" tableName="ACCESS_TOKEN" columnNames="TOKEN_HASH"/> <addUniqueConstraint constraintName="IDX_ACCESS_TOKEN_TOKEN_HASH" tableName="ACCESS_TOKEN" columnNames="TOKEN_HASH"/>
<addForeignKeyConstraint constraintName="IDX_ACCESS_TOKEN_FK0" baseTableName="ACCESS_TOKEN" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/> <addForeignKeyConstraint constraintName="IDX_ACCESS_TOKEN_FK0" baseTableName="ACCESS_TOKEN" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
@@ -73,7 +74,7 @@
<!-- ACTIVITY --> <!-- ACTIVITY -->
<!--================================================================================================--> <!--================================================================================================-->
<createTable tableName="ACTIVITY"> <createTable tableName="ACTIVITY">
<column name="ACTIVITY_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_ACTIVITY_PK" primaryKey="true"/> <column name="ACTIVITY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="USER_NAME" type="varchar(100)" nullable="false"/> <column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/> <column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="ACTIVITY_USER_NAME" type="varchar(100)" nullable="false"/> <column name="ACTIVITY_USER_NAME" type="varchar(100)" nullable="false"/>
@@ -83,6 +84,7 @@
<column name="ACTIVITY_DATE" type="datetime" nullable="false"/> <column name="ACTIVITY_DATE" type="datetime" nullable="false"/>
</createTable> </createTable>
<addPrimaryKey constraintName="IDX_ACTIVITY_PK" tableName="ACTIVITY" columnNames="ACTIVITY_ID"/>
<addForeignKeyConstraint constraintName="IDX_ACTIVITY_FK1" baseTableName="ACTIVITY" baseColumnNames="ACTIVITY_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/> <addForeignKeyConstraint constraintName="IDX_ACTIVITY_FK1" baseTableName="ACTIVITY" baseColumnNames="ACTIVITY_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
<addForeignKeyConstraint constraintName="IDX_ACTIVITY_FK0" baseTableName="ACTIVITY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/> <addForeignKeyConstraint constraintName="IDX_ACTIVITY_FK0" baseTableName="ACTIVITY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
@@ -106,7 +108,7 @@
<column name="USER_NAME" type="varchar(100)" nullable="false"/> <column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/> <column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="COMMIT_ID" type="varchar(100)" nullable="false"/> <column name="COMMIT_ID" type="varchar(100)" nullable="false"/>
<column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_COMMIT_COMMENT_PK" primaryKey="true" /> <column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="COMMENTED_USER_NAME" type="varchar(100)" nullable="false"/> <column name="COMMENTED_USER_NAME" type="varchar(100)" nullable="false"/>
<column name="CONTENT" type="text" nullable="false"/> <column name="CONTENT" type="text" nullable="false"/>
<column name="FILE_NAME" type="varchar(260)" nullable="true"/> <column name="FILE_NAME" type="varchar(260)" nullable="true"/>
@@ -117,13 +119,14 @@
<column name="ISSUE_ID" type="int" nullable="true"/> <column name="ISSUE_ID" type="int" nullable="true"/>
</createTable> </createTable>
<addPrimaryKey constraintName="IDX_COMMIT_COMMENT_PK" tableName="COMMIT_COMMENT" columnNames="COMMENT_ID"/>
<addForeignKeyConstraint constraintName="IDX_COMMIT_COMMENT_FK0" baseTableName="COMMIT_COMMENT" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/> <addForeignKeyConstraint constraintName="IDX_COMMIT_COMMENT_FK0" baseTableName="COMMIT_COMMENT" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<!--================================================================================================--> <!--================================================================================================-->
<!-- COMMIT_STATUS --> <!-- COMMIT_STATUS -->
<!--================================================================================================--> <!--================================================================================================-->
<createTable tableName="COMMIT_STATUS"> <createTable tableName="COMMIT_STATUS">
<column name="COMMIT_STATUS_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_COMMIT_STATUS_PK" primaryKey="true" /> <column name="COMMIT_STATUS_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="USER_NAME" type="varchar(100)" nullable="false"/> <column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/> <column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="COMMIT_ID" type="varchar(40)" nullable="false"/> <column name="COMMIT_ID" type="varchar(40)" nullable="false"/>
@@ -136,6 +139,7 @@
<column name="UPDATED_DATE" type="datetime" nullable="false"/> <column name="UPDATED_DATE" type="datetime" nullable="false"/>
</createTable> </createTable>
<addPrimaryKey constraintName="IDX_COMMIT_STATUS_PK" tableName="COMMIT_STATUS" columnNames="COMMIT_STATUS_ID"/>
<addUniqueConstraint constraintName="IDX_COMMIT_STATUS_1" tableName="COMMIT_STATUS" columnNames="USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT"/> <addUniqueConstraint constraintName="IDX_COMMIT_STATUS_1" tableName="COMMIT_STATUS" columnNames="USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT"/>
<addForeignKeyConstraint constraintName="IDX_COMMIT_STATUS_FK3" baseTableName="COMMIT_STATUS" baseColumnNames="CREATOR" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/> <addForeignKeyConstraint constraintName="IDX_COMMIT_STATUS_FK3" baseTableName="COMMIT_STATUS" baseColumnNames="CREATOR" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
<addForeignKeyConstraint constraintName="IDX_COMMIT_STATUS_FK2" baseTableName="COMMIT_STATUS" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/> <addForeignKeyConstraint constraintName="IDX_COMMIT_STATUS_FK2" baseTableName="COMMIT_STATUS" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
@@ -202,7 +206,7 @@
<column name="PULL_REQUEST" type="boolean" nullable="false"/> <column name="PULL_REQUEST" type="boolean" nullable="false"/>
</createTable> </createTable>
<addPrimaryKey constraintName="IDX_ISSUE_PK" tableName="ISSUE" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/> <addPrimaryKey constraintName="IDX_ISSUE_PK" tableName="ISSUE" columnNames="ISSUE_ID, USER_NAME, REPOSITORY_NAME"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_FK0" baseTableName="ISSUE" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/> <addForeignKeyConstraint constraintName="IDX_ISSUE_FK0" baseTableName="ISSUE" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_FK2" baseTableName="ISSUE" baseColumnNames="MILESTONE_ID" referencedTableName="MILESTONE" referencedColumnNames="MILESTONE_ID"/> <addForeignKeyConstraint constraintName="IDX_ISSUE_FK2" baseTableName="ISSUE" baseColumnNames="MILESTONE_ID" referencedTableName="MILESTONE" referencedColumnNames="MILESTONE_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_FK1" baseTableName="ISSUE" baseColumnNames="OPENED_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/> <addForeignKeyConstraint constraintName="IDX_ISSUE_FK1" baseTableName="ISSUE" baseColumnNames="OPENED_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
@@ -214,7 +218,7 @@
<column name="USER_NAME" type="varchar(100)" nullable="false"/> <column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/> <column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="ISSUE_ID" type="int" nullable="false"/> <column name="ISSUE_ID" type="int" nullable="false"/>
<column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_ISSUE_COMMENT_PK" primaryKey="true" /> <column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="ACTION" type="varchar(20)" nullable="false"/> <column name="ACTION" type="varchar(20)" nullable="false"/>
<column name="COMMENTED_USER_NAME" type="varchar(100)" nullable="false"/> <column name="COMMENTED_USER_NAME" type="varchar(100)" nullable="false"/>
<column name="CONTENT" type="text" nullable="false"/> <column name="CONTENT" type="text" nullable="false"/>
@@ -222,6 +226,7 @@
<column name="UPDATED_DATE" type="datetime" nullable="false"/> <column name="UPDATED_DATE" type="datetime" nullable="false"/>
</createTable> </createTable>
<addPrimaryKey constraintName="IDX_ISSUE_COMMENT_PK" tableName="ISSUE_COMMENT" columnNames="COMMENT_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_COMMENT_FK0" baseTableName="ISSUE_COMMENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/> <addForeignKeyConstraint constraintName="IDX_ISSUE_COMMENT_FK0" baseTableName="ISSUE_COMMENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
<!--================================================================================================--> <!--================================================================================================-->
@@ -343,4 +348,4 @@
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/> <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"/> <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> </changeSet>

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<createTable tableName="RELEASE">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="TAG" type="varchar(100)" nullable="false"/>
<column name="NAME" type="varchar(100)" nullable="false"/>
<column name="AUTHOR" type="varchar(100)" nullable="false"/>
<column name="CONTENT" type="text" nullable="true"/>
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_RELEASE_PK" tableName="RELEASE" columnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_FK0" baseTableName="RELEASE" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<createTable tableName="RELEASE_ASSET">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="TAG" type="varchar(100)" nullable="false"/>
<column name="RELEASE_ASSET_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="FILE_NAME" type="varchar(260)" nullable="false"/>
<column name="LABEL" type="varchar(100)" nullable="true"/>
<column name="SIZE" type="bigint" nullable="false"/>
<column name="UPLOADER" type="varchar(100)" nullable="false"/>
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_RELEASE_ASSET_PK" tableName="RELEASE_ASSET" columnNames="USER_NAME, REPOSITORY_NAME, TAG, FILE_NAME"/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_ASSET_FK1" baseTableName="RELEASE_ASSET" baseColumnNames="USER_NAME, REPOSITORY_NAME, TAG" referencedTableName="RELEASE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
<createTable tableName="ACCOUNT_FEDERATION">
<column name="ISSUER" type="varchar(100)" nullable="false"/>
<column name="SUBJECT" type="varchar(100)" nullable="false"/>
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_ACCOUNT_FEDERATION_PK" tableName="ACCOUNT_FEDERATION" columnNames="ISSUER, SUBJECT"/>
<addForeignKeyConstraint constraintName="IDX_ACCOUNT_FEDERATION_FK0" baseTableName="ACCOUNT_FEDERATION" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
</changeSet>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="REPOSITORY">
<column name="MERGE_OPTIONS" type="varchar(200)" nullable="false" defaultValue="merge-commit,squash,rebase"/>
<column name="DEFAULT_MERGE_OPTION" type="varchar(100)" nullable="false" defaultValue="merge-commit"/>
</addColumn>
</changeSet>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<dropForeignKeyConstraint baseTableName="RELEASE_ASSET" constraintName="IDX_RELEASE_ASSET_FK1"/>
<dropForeignKeyConstraint baseTableName="RELEASE" constraintName="IDX_RELEASE_FK0"/>
<dropPrimaryKey tableName="RELEASE" constraintName="IDX_RELEASE_PK"/>
<renameTable newTableName="RELEASE_TAG" oldTableName="RELEASE" />
<addPrimaryKey constraintName="IDX_RELEASE_TAG_PK" tableName="RELEASE_TAG" columnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_TAG_FK0" baseTableName="RELEASE_TAG" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<addForeignKeyConstraint constraintName="IDX_RELEASE_ASSET_FK0" baseTableName="RELEASE_ASSET" baseColumnNames="USER_NAME, REPOSITORY_NAME, TAG" referencedTableName="RELEASE_TAG" referencedColumnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
</changeSet>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<createTable tableName="ACCOUNT_EXTRA_MAIL_ADDRESS">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="EXTRA_MAIL_ADDRESS" type="varchar(100)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_ACCOUNT_EXTRA_MAIL_ADDRESS_PK" tableName="ACCOUNT_EXTRA_MAIL_ADDRESS" columnNames="USER_NAME, EXTRA_MAIL_ADDRESS"/>
<addUniqueConstraint constraintName="IDX_ACCOUNT_EXTRA_MAIL_ADDRESS_1" tableName="ACCOUNT_EXTRA_MAIL_ADDRESS" columnNames="EXTRA_MAIL_ADDRESS"/>
</changeSet>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<modifyDataType columnName="PASSWORD" newDataType="varchar(200)" tableName="ACCOUNT"/>
<delete tableName="ACCOUNT_EXTRA_MAIL_ADDRESS">
<where>EXTRA_MAIL_ADDRESS = ''</where>
</delete>
</changeSet>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="COMMIT_COMMENT">
<column name="ORIGINAL_COMMIT_ID" type="varchar(100)" nullable="true"/>
<column name="ORIGINAL_OLD_LINE" type="int" nullable="true"/>
<column name="ORIGINAL_NEW_LINE" type="int" nullable="true"/>
</addColumn>
<update tableName="COMMIT_COMMENT">
<column name="ORIGINAL_COMMIT_ID" valueComputed="COMMIT_ID"/>
<column name="ORIGINAL_OLD_LINE" valueComputed="OLD_LINE_NUMBER"/>
<column name="ORIGINAL_NEW_LINE" valueComputed="NEW_LINE_NUMBER"/>
</update>
<addNotNullConstraint columnName="ORIGINAL_COMMIT_ID" tableName="COMMIT_COMMENT" columnDataType="varchar(100)"/>
</changeSet>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<!--================================================================================================-->
<!-- SSH_KEY -->
<!--================================================================================================-->
<createTable tableName="GPG_KEY">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="KEY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="GPG_KEY_ID" type="bigint" nullable="false"/>
<column name="TITLE" type="varchar(100)" nullable="false"/>
<column name="PUBLIC_KEY" type="text" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_GPG_KEY_PK" tableName="GPG_KEY" columnNames="USER_NAME, GPG_KEY_ID"/>
<addForeignKeyConstraint constraintName="IDX_GPG_KEY_FK0" baseTableName="GPG_KEY" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
</changeSet>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="PULL_REQUEST">
<column name="IS_DRAFT" type="boolean" nullable="false" defaultValueBoolean="false" />
</addColumn>
</changeSet>

View File

@@ -1,42 +1,36 @@
import java.util.EnumSet import java.util.EnumSet
import javax.servlet._ import javax.servlet._
import gitbucket.core.controller.{ReleaseController, _} import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.servlet._ import gitbucket.core.servlet._
import gitbucket.core.util.Directory import gitbucket.core.util.Directory
import org.scalatra._ import org.scalatra._
class ScalatraBootstrap extends LifeCycle with SystemSettingsService { class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
override def init(context: ServletContext): Unit = { override def init(context: ServletContext) {
val settings = loadSystemSettings() val settings = loadSystemSettings()
if (settings.baseUrl.exists(_.startsWith("https://"))) { if(settings.baseUrl.exists(_.startsWith("https://"))) {
context.getSessionCookieConfig.setSecure(true) context.getSessionCookieConfig.setSecure(true)
} }
// Register TransactionFilter at first // Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter) context.addFilter("transactionFilter", new TransactionFilter)
context context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
.getFilterRegistration("transactionFilter")
.addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("gitAuthenticationFilter", new GitAuthenticationFilter) context.addFilter("gitAuthenticationFilter", new GitAuthenticationFilter)
context context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
.getFilterRegistration("gitAuthenticationFilter")
.addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter) context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter)
context context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
.getFilterRegistration("apiAuthenticationFilter")
.addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/*")
// Register controllers // Register controllers
context.mount(new PreProcessController, "/*") context.mount(new PreProcessController, "/*")
context.addFilter("pluginControllerFilter", new PluginControllerFilter) context.addFilter("pluginControllerFilter", new PluginControllerFilter)
context context.getFilterRegistration("pluginControllerFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
.getFilterRegistration("pluginControllerFilter")
.addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
@@ -53,17 +47,14 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
filter.mount(new MilestonesController, "/*") filter.mount(new MilestonesController, "/*")
filter.mount(new IssuesController, "/*") filter.mount(new IssuesController, "/*")
filter.mount(new PullRequestsController, "/*") filter.mount(new PullRequestsController, "/*")
filter.mount(new ReleaseController, "/*")
filter.mount(new RepositorySettingsController, "/*") filter.mount(new RepositorySettingsController, "/*")
context.addFilter("compositeScalatraFilter", filter) context.addFilter("compositeScalatraFilter", filter)
context context.getFilterRegistration("compositeScalatraFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
.getFilterRegistration("compositeScalatraFilter")
.addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
// Create GITBUCKET_HOME directory if it does not exist // Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(Directory.GitBucketHome) val dir = new java.io.File(Directory.GitBucketHome)
if (!dir.exists) { if(!dir.exists){
dir.mkdirs() dir.mkdirs()
} }
} }

View File

@@ -3,67 +3,49 @@ package gitbucket.core
import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration} import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration}
import io.github.gitbucket.solidbase.model.{Version, Module} import io.github.gitbucket.solidbase.model.{Version, Module}
object GitBucketCoreModule object GitBucketCoreModule extends Module("gitbucket-core",
extends Module( new Version("4.0.0",
"gitbucket-core", new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
new Version( new SqlMigration("update/gitbucket-core_4.0.sql")
"4.0.0", ),
new LiquibaseMigration("update/gitbucket-core_4.0.xml"), new Version("4.1.0"),
new SqlMigration("update/gitbucket-core_4.0.sql") new Version("4.2.0",
), new LiquibaseMigration("update/gitbucket-core_4.2.xml")
new Version("4.1.0"), ),
new Version("4.2.0", new LiquibaseMigration("update/gitbucket-core_4.2.xml")), new Version("4.2.1"),
new Version("4.2.1"), new Version("4.3.0"),
new Version("4.3.0"), new Version("4.4.0"),
new Version("4.4.0"), new Version("4.5.0"),
new Version("4.5.0"), new Version("4.6.0",
new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml")), new LiquibaseMigration("update/gitbucket-core_4.6.xml")
new Version( ),
"4.7.0", new Version("4.7.0",
new LiquibaseMigration("update/gitbucket-core_4.7.xml"), new LiquibaseMigration("update/gitbucket-core_4.7.xml"),
new SqlMigration("update/gitbucket-core_4.7.sql") new SqlMigration("update/gitbucket-core_4.7.sql")
), ),
new Version("4.7.1"), new Version("4.7.1"),
new Version("4.8"), new Version("4.8"),
new Version("4.9.0", new LiquibaseMigration("update/gitbucket-core_4.9.xml")), new Version("4.9.0",
new Version("4.10.0"), new LiquibaseMigration("update/gitbucket-core_4.9.xml")
new Version("4.11.0", new LiquibaseMigration("update/gitbucket-core_4.11.xml")), ),
new Version("4.12.0"), new Version("4.10.0"),
new Version("4.12.1"), new Version("4.11.0",
new Version("4.13.0"), new LiquibaseMigration("update/gitbucket-core_4.11.xml")
new Version( ),
"4.14.0", new Version("4.12.0"),
new LiquibaseMigration("update/gitbucket-core_4.14.xml"), new Version("4.12.1"),
new SqlMigration("update/gitbucket-core_4.14.sql") new Version("4.13.0"),
), new Version("4.14.0",
new Version("4.14.1"), new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
new Version("4.15.0"), new SqlMigration("update/gitbucket-core_4.14.sql")
new Version("4.16.0"), ),
new Version("4.17.0"), new Version("4.14.1"),
new Version("4.18.0"), new Version("4.15.0"),
new Version("4.19.0"), new Version("4.16.0"),
new Version("4.19.1"), new Version("4.17.0"),
new Version("4.19.2"), new Version("4.18.0"),
new Version("4.19.3"), new Version("4.19.0"),
new Version("4.20.0"), new Version("4.19.1"),
new Version("4.21.0", new LiquibaseMigration("update/gitbucket-core_4.21.xml")), new Version("4.19.2"),
new Version("4.21.1"), new Version("4.19.3")
new Version("4.21.2"), )
new Version("4.22.0", new LiquibaseMigration("update/gitbucket-core_4.22.xml")),
new Version("4.23.0", new LiquibaseMigration("update/gitbucket-core_4.23.xml")),
new Version("4.23.1"),
new Version("4.24.0", new LiquibaseMigration("update/gitbucket-core_4.24.xml")),
new Version("4.24.1"),
new Version("4.25.0", new LiquibaseMigration("update/gitbucket-core_4.25.xml")),
new Version("4.26.0"),
new Version("4.27.0", new LiquibaseMigration("update/gitbucket-core_4.27.xml")),
new Version("4.28.0"),
new Version("4.29.0"),
new Version("4.30.0"),
new Version("4.30.1"),
new Version("4.31.0", new LiquibaseMigration("update/gitbucket-core_4.31.xml")),
new Version("4.31.1"),
new Version("4.31.2"),
new Version("4.32.0", new LiquibaseMigration("update/gitbucket-core_4.32.xml")),
new Version("4.33.0")
)

View File

@@ -1,9 +0,0 @@
package gitbucket.core.api
case class AddACollaborator(permission: String) {
val role: String = permission match {
case "admin" => "ADMIN"
case "push" => "DEVELOPER"
case "pull" => "GUEST"
}
}

View File

@@ -6,14 +6,13 @@ import gitbucket.core.util.RepositoryName
* https://developer.github.com/v3/repos/#get-branch * https://developer.github.com/v3/repos/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection * https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/ */
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtection)( case class ApiBranch(
repositoryName: RepositoryName name: String,
) extends FieldSerializable { commit: ApiBranchCommit,
val _links = protection: ApiBranchProtection)(repositoryName:RepositoryName) extends FieldSerializable {
Map( def _links = Map(
"self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"), "self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"),
"html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}") "html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}"))
)
} }
case class ApiBranchCommit(sha: String) case class ApiBranchCommit(sha: String)

View File

@@ -4,22 +4,17 @@ import gitbucket.core.service.ProtectedBranchService
import org.json4s._ import org.json4s._
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]) { case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]){
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone) def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
} }
object ApiBranchProtection { object ApiBranchProtection{
/** form for enabling-and-disabling-branch-protection */ /** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtection) case class EnablingAndDisabling(protection: ApiBranchProtection)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
ApiBranchProtection( enabled = info.enabled,
enabled = info.enabled, required_status_checks = Some(Status(EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators), info.contexts)))
required_status_checks = Some(
Status(EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators), info.contexts)
)
)
val statusNone = Status(Off, Seq.empty) val statusNone = Status(Off, Seq.empty)
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String]) case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
sealed class EnforcementLevel(val name: String) sealed class EnforcementLevel(val name: String)
@@ -27,28 +22,25 @@ object ApiBranchProtection {
case object NonAdmins extends EnforcementLevel("non_admins") case object NonAdmins extends EnforcementLevel("non_admins")
case object Everyone extends EnforcementLevel("everyone") case object Everyone extends EnforcementLevel("everyone")
object EnforcementLevel { object EnforcementLevel {
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = if(enabled){
if (enabled) { if(includeAdministrators){
if (includeAdministrators) { Everyone
Everyone }else{
} else { NonAdmins
NonAdmins
}
} else {
Off
} }
}else{
Off
}
} }
implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel]( implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => (
format => {
( case JString("off") => Off
{ case JString("non_admins") => NonAdmins
case JString("off") => Off case JString("everyone") => Everyone
case JString("non_admins") => NonAdmins },
case JString("everyone") => Everyone {
}, { case x: EnforcementLevel => JString(x.name)
case x: EnforcementLevel => JString(x.name) }
} ))
)
)
} }

View File

@@ -2,6 +2,7 @@ package gitbucket.core.api
import gitbucket.core.model.{Account, CommitState, CommitStatus} import gitbucket.core.model.{Account, CommitState, CommitStatus}
/** /**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*/ */
@@ -10,22 +11,15 @@ case class ApiCombinedCommitStatus(
sha: String, sha: String,
total_count: Int, total_count: Int,
statuses: Iterable[ApiCommitStatus], statuses: Iterable[ApiCommitStatus],
repository: ApiRepository repository: ApiRepository){
) {
// val commit_url = ApiPath(s"/api/v3/repos/${repository.full_name}/${sha}") // val commit_url = ApiPath(s"/api/v3/repos/${repository.full_name}/${sha}")
val url = ApiPath(s"/api/v3/repos/${repository.full_name}/commits/${sha}/status") val url = ApiPath(s"/api/v3/repos/${repository.full_name}/commits/${sha}/status")
} }
object ApiCombinedCommitStatus { object ApiCombinedCommitStatus {
def apply( def apply(sha:String, statuses: Iterable[(CommitStatus, Account)], repository:ApiRepository): ApiCombinedCommitStatus = ApiCombinedCommitStatus(
sha: String, state = CommitState.combine(statuses.map(_._1.state).toSet).name,
statuses: Iterable[(CommitStatus, Account)], sha = sha,
repository: ApiRepository total_count= statuses.size,
): ApiCombinedCommitStatus = statuses = statuses.map{ case (s, a)=> ApiCommitStatus(s, ApiUser(a)) },
ApiCombinedCommitStatus( repository = repository)
state = CommitState.combine(statuses.map(_._1.state).toSet).name,
sha = sha,
total_count = statuses.size,
statuses = statuses.map { case (s, a) => ApiCommitStatus(s, ApiUser(a)) },
repository = repository
)
} }

View File

@@ -5,32 +5,25 @@ import gitbucket.core.util.RepositoryName
import java.util.Date import java.util.Date
/** /**
* https://developer.github.com/v3/issues/comments/ * https://developer.github.com/v3/issues/comments/
*/ */
case class ApiComment(id: Int, user: ApiUser, body: String, created_at: Date, updated_at: Date)( case class ApiComment(
repositoryName: RepositoryName, id: Int,
issueId: Int, user: ApiUser,
isPullRequest: Boolean body: String,
) { created_at: Date,
val html_url = ApiPath( updated_at: Date)(repositoryName: RepositoryName, issueId: Int, isPullRequest: Boolean){
s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${issueId}#comment-${id}" val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${issueId}#comment-${id}")
)
} }
object ApiComment { object ApiComment{
def apply( def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser, isPullRequest: Boolean): ApiComment =
comment: IssueComment,
repositoryName: RepositoryName,
issueId: Int,
user: ApiUser,
isPullRequest: Boolean
): ApiComment =
ApiComment( ApiComment(
id = comment.commentId, id = comment.commentId,
user = user, user = user,
body = comment.content, body = comment.content,
created_at = comment.registeredDate, created_at = comment.registeredDate,
updated_at = comment.updatedDate updated_at = comment.updatedDate)(repositoryName, issueId, isPullRequest)
)(repositoryName, issueId, isPullRequest)
} }

View File

@@ -20,31 +20,38 @@ case class ApiCommit(
removed: List[String], removed: List[String],
modified: List[String], modified: List[String],
author: ApiPersonIdent, author: ApiPersonIdent,
committer: ApiPersonIdent committer: ApiPersonIdent)(repositoryName:RepositoryName, urlIsHtmlUrl: Boolean) extends FieldSerializable{
)(repositoryName: RepositoryName) val url = if(urlIsHtmlUrl){
extends FieldSerializable { ApiPath(s"/${repositoryName.fullName}/commit/${id}")
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}") }else{
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}") ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
} }
val html_url = if(urlIsHtmlUrl){
object ApiCommit { None
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = { }else{
val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false) Some(ApiPath(s"/${repositoryName.fullName}/commit/${id}"))
ApiCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.commitTime,
added = diffs.collect {
case x if x.changeType == DiffEntry.ChangeType.ADD => x.newPath
},
removed = diffs.collect {
case x if x.changeType == DiffEntry.ChangeType.DELETE => x.oldPath
},
modified = diffs.collect {
case x if x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE => x.newPath
},
author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(repositoryName)
} }
} }
object ApiCommit{
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): ApiCommit = {
val diffs = JGitUtil.getDiffs(git, commit.id, false)
ApiCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.commitTime,
added = diffs._1.collect {
case x if x.changeType == DiffEntry.ChangeType.ADD => x.newPath
},
removed = diffs._1.collect {
case x if x.changeType == DiffEntry.ChangeType.DELETE => x.oldPath
},
modified = diffs._1.collect {
case x if x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE => x.newPath
},
author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(repositoryName, urlIsHtmlUrl)
}
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
}

View File

@@ -4,6 +4,7 @@ import gitbucket.core.api.ApiCommitListItem._
import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName import gitbucket.core.util.RepositoryName
/** /**
* https://developer.github.com/v3/repos/commits/ * https://developer.github.com/v3/repos/commits/
*/ */
@@ -12,33 +13,30 @@ case class ApiCommitListItem(
commit: Commit, commit: Commit,
author: Option[ApiUser], author: Option[ApiUser],
committer: Option[ApiUser], committer: Option[ApiUser],
parents: Seq[Parent] parents: Seq[Parent])(repositoryName: RepositoryName) {
)(repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}") val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
} }
object ApiCommitListItem { object ApiCommitListItem {
def apply(commit: CommitInfo, repositoryName: RepositoryName): ApiCommitListItem = def apply(commit: CommitInfo, repositoryName: RepositoryName): ApiCommitListItem = ApiCommitListItem(
ApiCommitListItem( sha = commit.id,
sha = commit.id, commit = Commit(
commit = Commit( message = commit.fullMessage,
message = commit.fullMessage, author = ApiPersonIdent.author(commit),
author = ApiPersonIdent.author(commit), committer = ApiPersonIdent.committer(commit)
committer = ApiPersonIdent.committer(commit)
)(commit.id, repositoryName), )(commit.id, repositoryName),
author = None, author = None,
committer = None, committer = None,
parents = commit.parents.map(Parent(_)(repositoryName)) parents = commit.parents.map(Parent(_)(repositoryName)))(repositoryName)
)(repositoryName)
case class Parent(sha: String)(repositoryName: RepositoryName) { case class Parent(sha: String)(repositoryName: RepositoryName){
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}") val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
} }
case class Commit(message: String, author: ApiPersonIdent, committer: ApiPersonIdent)( case class Commit(
sha: String, message: String,
repositoryName: RepositoryName author: ApiPersonIdent,
) { committer: ApiPersonIdent)(sha:String, repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${sha}") val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${sha}")
} }
} }

View File

@@ -5,6 +5,7 @@ import gitbucket.core.util.RepositoryName
import java.util.Date import java.util.Date
/** /**
* https://developer.github.com/v3/repos/statuses/#create-a-status * https://developer.github.com/v3/repos/statuses/#create-a-status
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
@@ -22,16 +23,16 @@ case class ApiCommitStatus(
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}/statuses") val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}/statuses")
} }
object ApiCommitStatus { object ApiCommitStatus {
def apply(status: CommitStatus, creator: ApiUser): ApiCommitStatus = def apply(status: CommitStatus, creator:ApiUser): ApiCommitStatus = ApiCommitStatus(
ApiCommitStatus( created_at = status.registeredDate,
created_at = status.registeredDate, updated_at = status.updatedDate,
updated_at = status.updatedDate, state = status.state.name,
state = status.state.name, target_url = status.targetUrl,
target_url = status.targetUrl, description= status.description,
description = status.description, id = status.commitStatusId,
id = status.commitStatusId, context = status.context,
context = status.context, creator = creator
creator = creator )(status.commitId, RepositoryName(status))
)(status.commitId, RepositoryName(status))
} }

View File

@@ -51,25 +51,20 @@ object ApiCommits {
patch: String patch: String
) )
def apply(
repositoryName: RepositoryName, def apply(repositoryName: RepositoryName, commitInfo: CommitInfo, diffs: Seq[DiffInfo], author: Account, committer: Account,
commitInfo: CommitInfo, commentCount: Int): ApiCommits = {
diffs: Seq[DiffInfo],
author: Account,
committer: Account,
commentCount: Int
): ApiCommits = {
val files = diffs.map { diff => val files = diffs.map { diff =>
var additions = 0 var additions = 0
var deletions = 0 var deletions = 0
diff.patch.getOrElse("").split("\n").foreach { line => diff.patch.getOrElse("").split("\n").foreach { line =>
if (line.startsWith("+")) additions = additions + 1 if(line.startsWith("+")) additions = additions + 1
if (line.startsWith("-")) deletions = deletions + 1 if(line.startsWith("-")) deletions = deletions + 1
} }
File( 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, additions = additions,
deletions = deletions, deletions = deletions,
changes = additions + deletions, changes = additions + deletions,
@@ -80,12 +75,12 @@ object ApiCommits {
case ChangeType.RENAME => "renamed" case ChangeType.RENAME => "renamed"
case ChangeType.COPY => "copied" case ChangeType.COPY => "copied"
}, },
raw_url = if (diff.changeType == ChangeType.DELETE) { raw_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.parents.head}/${diff.oldPath}") ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.parents.head}/${diff.oldPath}")
} else { } else {
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.id}/${diff.newPath}") ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.id}/${diff.newPath}")
}, },
blob_url = if (diff.changeType == ChangeType.DELETE) { blob_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.parents.head}/${diff.oldPath}") ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.parents.head}/${diff.oldPath}")
} else { } else {
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.id}/${diff.newPath}") ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.id}/${diff.newPath}")
@@ -98,7 +93,7 @@ object ApiCommits {
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
sha = commitInfo.id, sha = commitInfo.id,
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"), html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
comment_url = ApiPath(""), // TODO no API for commit comment comment_url = ApiPath(""),
commit = Commit( commit = Commit(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
author = ApiPersonIdent.author(commitInfo), author = ApiPersonIdent.author(commitInfo),

View File

@@ -11,29 +11,18 @@ case class ApiContents(
path: String, path: String,
sha: String, sha: String,
content: Option[String], content: Option[String],
encoding: Option[String] encoding: Option[String])(repositoryName: RepositoryName){
)(repositoryName: RepositoryName) {
val download_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/raw/${sha}/${path}") val download_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/raw/${sha}/${path}")
} }
object ApiContents { object ApiContents{
def apply(fileInfo: FileInfo, repositoryName: RepositoryName, content: Option[Array[Byte]]): ApiContents = { def apply(fileInfo: FileInfo, repositoryName: RepositoryName, content: Option[Array[Byte]]): ApiContents = {
if (fileInfo.isDirectory) { if(fileInfo.isDirectory) {
ApiContents("dir", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName) ApiContents("dir", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName)
} else { } else {
content content.map(arr =>
.map( ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, Some(Base64.getEncoder.encodeToString(arr)), Some("base64"))(repositoryName)
arr => ).getOrElse(ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName))
ApiContents(
"file",
fileInfo.name,
fileInfo.path,
fileInfo.commitId,
Some(Base64.getEncoder.encodeToString(arr)),
Some("base64")
)(repositoryName)
)
.getOrElse(ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName))
} }
} }
} }

View File

@@ -1,3 +1,3 @@
package gitbucket.core.api package gitbucket.core.api
case class ApiEndPoint(rate_limit_url: ApiPath = ApiPath("/api/v3/rate_limit")) case class ApiEndPoint(rate_limit_url: ApiPath = ApiPath("/api/v3/rate_limit"))

View File

@@ -1,3 +1,5 @@
package gitbucket.core.api package gitbucket.core.api
case class ApiError(message: String, documentation_url: Option[String] = None) case class ApiError(
message: String,
documentation_url: Option[String] = None)

View File

@@ -1,20 +0,0 @@
package gitbucket.core.api
import java.util.Date
import gitbucket.core.model.Account
case class ApiGroup(login: String, description: Option[String], created_at: Date) {
val id = 0 // dummy id
val url = ApiPath(s"/api/v3/orgs/${login}")
val html_url = ApiPath(s"/${login}")
val avatar_url = ApiPath(s"/${login}/_avatar")
}
object ApiGroup {
def apply(group: Account): ApiGroup = ApiGroup(
login = group.userName,
description = group.description,
created_at = group.registeredDate
)
}

View File

@@ -5,6 +5,7 @@ import gitbucket.core.util.RepositoryName
import java.util.Date import java.util.Date
/** /**
* https://developer.github.com/v3/issues/ * https://developer.github.com/v3/issues/
*/ */
@@ -12,48 +13,33 @@ case class ApiIssue(
number: Int, number: Int,
title: String, title: String,
user: ApiUser, user: ApiUser,
assignee: Option[ApiUser], // labels,
labels: List[ApiLabel],
state: String, state: String,
created_at: Date, created_at: Date,
updated_at: Date, updated_at: Date,
body: String body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
)(repositoryName: RepositoryName, isPullRequest: Boolean) {
val id = 0 // dummy id
val assignees = List(assignee).flatten
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments") val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}") val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
val pull_request = if (isPullRequest) { val pull_request = if (isPullRequest) {
Some( Some(Map(
Map( "url" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${number}"),
"url" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${number}"), "html_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}")
"html_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}") // "diff_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.diff"),
// "diff_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.diff"), // "patch_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.patch")
// "patch_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.patch") ))
)
)
} else { } else {
None None
} }
} }
object ApiIssue { object ApiIssue{
def apply( def apply(issue: Issue, repositoryName: RepositoryName, user: ApiUser): ApiIssue =
issue: Issue,
repositoryName: RepositoryName,
user: ApiUser,
assignee: Option[ApiUser],
labels: List[ApiLabel]
): ApiIssue =
ApiIssue( ApiIssue(
number = issue.issueId, number = issue.issueId,
title = issue.title, title = issue.title,
user = user, user = user,
assignee = assignee, state = if(issue.closed){ "closed" }else{ "open" },
labels = labels, body = issue.content.getOrElse(""),
state = if (issue.closed) { "closed" } else { "open" },
body = issue.content.getOrElse(""),
created_at = issue.registeredDate, created_at = issue.registeredDate,
updated_at = issue.updatedDate updated_at = issue.updatedDate)(repositoryName, issue.isPullRequest)
)(repositoryName, issue.isPullRequest)
} }

View File

@@ -4,16 +4,18 @@ import gitbucket.core.model.Label
import gitbucket.core.util.RepositoryName import gitbucket.core.util.RepositoryName
/** /**
* https://developer.github.com/v3/issues/labels/ * https://developer.github.com/v3/issues/labels/
*/ */
case class ApiLabel(name: String, color: String)(repositoryName: RepositoryName) { case class ApiLabel(
name: String,
color: String)(repositoryName: RepositoryName){
var url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/labels/${name}") var url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/labels/${name}")
} }
object ApiLabel { object ApiLabel{
def apply(label: Label, repositoryName: RepositoryName): ApiLabel = def apply(label:Label, repositoryName: RepositoryName): ApiLabel =
ApiLabel( ApiLabel(
name = label.labelName, name = label.labelName,
color = label.color color = label.color
)(repositoryName) )(repositoryName)
} }

View File

@@ -4,11 +4,22 @@ import gitbucket.core.util.JGitUtil.CommitInfo
import java.util.Date import java.util.Date
case class ApiPersonIdent(name: String, email: String, date: Date)
case class ApiPersonIdent(
name: String,
email: String,
date: Date)
object ApiPersonIdent { object ApiPersonIdent {
def author(commit: CommitInfo): ApiPersonIdent = def author(commit: CommitInfo): ApiPersonIdent =
ApiPersonIdent(name = commit.authorName, email = commit.authorEmailAddress, date = commit.authorTime) ApiPersonIdent(
name = commit.authorName,
email = commit.authorEmailAddress,
date = commit.authorTime)
def committer(commit: CommitInfo): ApiPersonIdent = def committer(commit: CommitInfo): ApiPersonIdent =
ApiPersonIdent(name = commit.committerName, email = commit.committerEmailAddress, date = commit.commitTime) ApiPersonIdent(
name = commit.committerName,
email = commit.committerEmailAddress,
date = commit.commitTime)
} }

View File

@@ -1,17 +0,0 @@
package gitbucket.core.api
import gitbucket.core.plugin.{PluginRegistry, PluginInfo}
case class ApiPlugin(
id: String,
name: String,
version: String,
description: String,
jarFileName: String
)
object ApiPlugin {
def apply(plugin: PluginInfo): ApiPlugin = {
ApiPlugin(plugin.pluginId, plugin.pluginName, plugin.pluginVersion, plugin.description, plugin.pluginJar.getName)
}
}

View File

@@ -8,7 +8,6 @@ import java.util.Date
*/ */
case class ApiPullRequest( case class ApiPullRequest(
number: Int, number: Int,
state: String,
updated_at: Date, updated_at: Date,
created_at: Date, created_at: Date,
head: ApiPullRequest.Commit, head: ApiPullRequest.Commit,
@@ -20,55 +19,56 @@ case class ApiPullRequest(
title: String, title: String,
body: String, body: String,
user: ApiUser, user: ApiUser,
labels: List[ApiLabel], assignee: Option[ApiUser]){
assignee: Option[ApiUser], val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
draft: Option[Boolean]
) {
val id = 0 // dummy id
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff") //val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch") //val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
val url = ApiPath(s"${base.repo.url.path}/pulls/${number}") val url = ApiPath(s"${base.repo.url.path}/pulls/${number}")
//val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}") //val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}")
val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits") val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits")
val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments") val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments")
val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}") val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}")
val comments_url = ApiPath(s"${base.repo.url.path}/issues/${number}/comments") val comments_url = ApiPath(s"${base.repo.url.path}/issues/${number}/comments")
val statuses_url = ApiPath(s"${base.repo.url.path}/statuses/${head.sha}") val statuses_url = ApiPath(s"${base.repo.url.path}/statuses/${head.sha}")
} }
object ApiPullRequest { object ApiPullRequest{
def apply( def apply(
issue: Issue, issue: Issue,
pullRequest: PullRequest, pullRequest: PullRequest,
headRepo: ApiRepository, headRepo: ApiRepository,
baseRepo: ApiRepository, baseRepo: ApiRepository,
user: ApiUser, user: ApiUser,
labels: List[ApiLabel],
assignee: Option[ApiUser], assignee: Option[ApiUser],
mergedComment: Option[(IssueComment, Account)] mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest = ): ApiPullRequest =
ApiPullRequest( ApiPullRequest(
number = issue.issueId, number = issue.issueId,
state = if (issue.closed) "closed" else "open",
updated_at = issue.updatedDate, updated_at = issue.updatedDate,
created_at = issue.registeredDate, created_at = issue.registeredDate,
head = Commit(sha = pullRequest.commitIdTo, ref = pullRequest.requestBranch, repo = headRepo)(issue.userName), head = Commit(
base = Commit(sha = pullRequest.commitIdFrom, ref = pullRequest.branch, repo = baseRepo)(issue.userName), sha = pullRequest.commitIdTo,
mergeable = None, // TODO: need check mergeable. ref = pullRequest.requestBranch,
merged = mergedComment.isDefined, repo = headRepo)(issue.userName),
merged_at = mergedComment.map { case (comment, _) => comment.registeredDate }, base = Commit(
merged_by = mergedComment.map { case (_, account) => ApiUser(account) }, sha = pullRequest.commitIdFrom,
title = issue.title, ref = pullRequest.branch,
body = issue.content.getOrElse(""), repo = baseRepo)(issue.userName),
user = user, mergeable = None, // TODO: need check mergeable.
labels = labels, merged = mergedComment.isDefined,
assignee = assignee, merged_at = mergedComment.map { case (comment, _) => comment.registeredDate },
draft = Some(pullRequest.isDraft) merged_by = mergedComment.map { case (_, account) => ApiUser(account) },
title = issue.title,
body = issue.content.getOrElse(""),
user = user,
assignee = assignee
) )
case class Commit(sha: String, ref: String, repo: ApiRepository)(baseOwner: String) { case class Commit(
val label = if (baseOwner == repo.owner.login) { ref } else { s"${repo.owner.login}:${ref}" } sha: String,
ref: String,
repo: ApiRepository)(baseOwner:String){
val label = if( baseOwner == repo.owner.login ){ ref } else { s"${repo.owner.login}:${ref}" }
val user = repo.owner val user = repo.owner
} }

View File

@@ -17,11 +17,10 @@ case class ApiPullRequestReviewComment(
commit_id: String, // "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", commit_id: String, // "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
// "original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", // "original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
user: ApiUser, user: ApiUser,
body: String, // "Maybe you should use more emoji on this line.", body: String, // "Maybe you should use more emojji on this line.",
created_at: Date, // "2015-05-05T23:40:27Z", created_at: Date, // "2015-05-05T23:40:27Z",
updated_at: Date // "2015-05-05T23:40:27Z", updated_at: Date // "2015-05-05T23:40:27Z",
)(repositoryName: RepositoryName, issueId: Int) )(repositoryName:RepositoryName, issueId: Int) extends FieldSerializable {
extends FieldSerializable {
// "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692", // "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692",
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/comments/${id}") val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/comments/${id}")
// "html_url": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692", // "html_url": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692",
@@ -41,28 +40,22 @@ case class ApiPullRequestReviewComment(
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1" "href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1"
} }
} }
*/ */
val _links = Map( val _links = Map(
"self" -> Map("href" -> url), "self" -> Map("href" -> url),
"html" -> Map("href" -> html_url), "html" -> Map("href" -> html_url),
"pull_request" -> Map("href" -> pull_request_url) "pull_request" -> Map("href" -> pull_request_url))
)
} }
object ApiPullRequestReviewComment { object ApiPullRequestReviewComment{
def apply( def apply(comment: CommitComment, commentedUser: ApiUser, repositoryName: RepositoryName, issueId: Int): ApiPullRequestReviewComment =
comment: CommitComment,
commentedUser: ApiUser,
repositoryName: RepositoryName,
issueId: Int
): ApiPullRequestReviewComment =
new ApiPullRequestReviewComment( new ApiPullRequestReviewComment(
id = comment.commentId, id = comment.commentId,
path = comment.fileName.getOrElse(""), path = comment.fileName.getOrElse(""),
commit_id = comment.commitId, commit_id = comment.commitId,
user = commentedUser, user = commentedUser,
body = comment.content, body = comment.content,
created_at = comment.registeredDate, created_at = comment.registeredDate,
updated_at = comment.updatedDate updated_at = comment.updatedDate
)(repositoryName, issueId) )(repositoryName, issueId)
} }

View File

@@ -5,5 +5,7 @@ import gitbucket.core.model.Account
case class ApiPusher(name: String, email: String) case class ApiPusher(name: String, email: String)
object ApiPusher { object ApiPusher {
def apply(user: Account): ApiPusher = ApiPusher(name = user.userName, email = user.mailAddress) def apply(user: Account): ApiPusher = ApiPusher(
} name = user.userName,
email = user.mailAddress)
}

View File

@@ -1,42 +0,0 @@
package gitbucket.core.api
import gitbucket.core.model.{Account, ReleaseAsset, ReleaseTag}
import gitbucket.core.util.RepositoryName
case class ApiReleaseAsset(name: String, size: Long)(tag: String, fileName: String, repositoryName: RepositoryName) {
val label = name
val file_id = fileName
val browser_download_url = ApiPath(
s"/${repositoryName.fullName}/releases/${tag}/assets/${fileName}"
)
}
object ApiReleaseAsset {
def apply(asset: ReleaseAsset, repositoryName: RepositoryName): ApiReleaseAsset =
ApiReleaseAsset(asset.label, asset.size)(asset.tag, asset.fileName, repositoryName)
}
case class ApiRelease(
name: String,
tag_name: String,
body: Option[String],
author: ApiUser,
assets: Seq[ApiReleaseAsset]
)
object ApiRelease {
def apply(
release: ReleaseTag,
assets: Seq[ReleaseAsset],
author: Account,
repositoryName: RepositoryName
): ApiRelease =
ApiRelease(
release.name,
release.tag,
release.content,
ApiUser(author),
assets.map { asset =>
ApiReleaseAsset(asset, repositoryName)
}
)
}

View File

@@ -3,6 +3,7 @@ package gitbucket.core.api
import gitbucket.core.model.{Account, Repository} import gitbucket.core.model.{Account, Repository}
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
// https://developer.github.com/v3/repos/ // https://developer.github.com/v3/repos/
case class ApiRepository( case class ApiRepository(
name: String, name: String,
@@ -12,51 +13,56 @@ case class ApiRepository(
forks: Int, forks: Int,
`private`: Boolean, `private`: Boolean,
default_branch: String, default_branch: String,
owner: ApiUser owner: ApiUser)(urlIsHtmlUrl: Boolean) {
) {
val id = 0 // dummy id
val forks_count = forks val forks_count = forks
val watchers_count = watchers val watchers_count = watchers
val url = ApiPath(s"/api/v3/repos/${full_name}") val url = if(urlIsHtmlUrl){
val http_url = ApiPath(s"/git/${full_name}.git") ApiPath(s"/${full_name}")
} else {
ApiPath(s"/api/v3/repos/${full_name}")
}
val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git") val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}") val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s":${full_name}.git")) val ssh_url = Some(SshPath(s":${full_name}.git"))
} }
object ApiRepository { object ApiRepository{
def apply( def apply(
repository: Repository, repository: Repository,
owner: ApiUser, owner: ApiUser,
forkedCount: Int = 0, forkedCount: Int =0,
watchers: Int = 0 watchers: Int = 0,
): ApiRepository = urlIsHtmlUrl: Boolean = false): ApiRepository =
ApiRepository( ApiRepository(
name = repository.repositoryName, name = repository.repositoryName,
full_name = s"${repository.userName}/${repository.repositoryName}", full_name = s"${repository.userName}/${repository.repositoryName}",
description = repository.description.getOrElse(""), description = repository.description.getOrElse(""),
watchers = watchers, watchers = watchers,
forks = forkedCount, forks = forkedCount,
`private` = repository.isPrivate, `private` = repository.isPrivate,
default_branch = repository.defaultBranch, default_branch = repository.defaultBranch,
owner = owner owner = owner
) )(urlIsHtmlUrl)
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository = def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount = repositoryInfo.forkedCount) ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository = def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
this(repositoryInfo, ApiUser(owner)) this(repositoryInfo.repository, ApiUser(owner))
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
def forDummyPayload(owner: ApiUser): ApiRepository = def forDummyPayload(owner: ApiUser): ApiRepository =
ApiRepository( ApiRepository(
name = "dummy", name = "dummy",
full_name = s"${owner.login}/dummy", full_name = s"${owner.login}/dummy",
description = "", description = "",
watchers = 0, watchers = 0,
forks = 0, forks = 0,
`private` = false, `private` = false,
default_branch = "master", default_branch = "master",
owner = owner owner = owner
) )(true)
} }

View File

@@ -4,11 +4,16 @@ import gitbucket.core.model.Account
import java.util.Date import java.util.Date
case class ApiUser(login: String, email: String, `type`: String, site_admin: Boolean, created_at: Date) {
val id = 0 // dummy id case class ApiUser(
val url = ApiPath(s"/api/v3/users/${login}") login: String,
val html_url = ApiPath(s"/${login}") email: String,
val avatar_url = ApiPath(s"/${login}/_avatar") `type`: String,
site_admin: Boolean,
created_at: Date) {
val url = ApiPath(s"/api/v3/users/${login}")
val html_url = ApiPath(s"/${login}")
val avatar_url = ApiPath(s"/${login}/_avatar")
// val followers_url = ApiPath(s"/api/v3/users/${login}/followers") // val followers_url = ApiPath(s"/api/v3/users/${login}/followers")
// val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}") // val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}")
// val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}") // val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}")
@@ -20,11 +25,12 @@ case class ApiUser(login: String, email: String, `type`: String, site_admin: Boo
// val received_events_url = ApiPath(s"/api/v3/users/${login}/received_events") // val received_events_url = ApiPath(s"/api/v3/users/${login}/received_events")
} }
object ApiUser {
object ApiUser{
def apply(user: Account): ApiUser = ApiUser( def apply(user: Account): ApiUser = ApiUser(
login = user.userName, login = user.userName,
email = user.mailAddress, email = user.mailAddress,
`type` = if (user.isGroupAccount) { "Organization" } else { "User" }, `type` = if(user.isGroupAccount){ "Organization" } else { "User" },
site_admin = user.isAdmin, site_admin = user.isAdmin,
created_at = user.registeredDate created_at = user.registeredDate
) )

View File

@@ -1,13 +0,0 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/repos/contents/#create-a-file
*/
case class CreateAFile(
message: String,
content: String,
sha: Option[String],
branch: Option[String],
committer: Option[ApiPusher],
author: Option[ApiPusher]
)

View File

@@ -1,8 +0,0 @@
package gitbucket.core.api
case class CreateAGroup(
login: String,
admin: String,
profile_name: Option[String],
url: Option[String]
)

View File

@@ -1,18 +1,18 @@
package gitbucket.core.api package gitbucket.core.api
/** /**
* https://developer.github.com/v3/issues/labels/#create-a-label * https://developer.github.com/v3/issues/labels/#create-a-label
* api form * api form
*/ */
case class CreateALabel( case class CreateALabel(
name: String, name: String,
color: String color: String
) { ) {
def isValid: Boolean = { def isValid: Boolean = {
name.length <= 100 && name.length<=100 &&
!name.startsWith("_") && !name.startsWith("_") &&
!name.startsWith("-") && !name.startsWith("-") &&
color.length == 6 && color.length==6 &&
color.matches("[a-fA-F0-9+_.]+") color.matches("[a-fA-F0-9+_.]+")
} }
} }

View File

@@ -1,17 +0,0 @@
package gitbucket.core.api
case class CreateAPullRequest(
title: String,
head: String,
base: String,
body: Option[String],
maintainer_can_modify: Option[Boolean],
draft: Option[Boolean]
)
case class CreateAPullRequestAlt(
issue: Integer,
head: String,
base: String,
maintainer_can_modify: Option[Boolean]
)

View File

@@ -1,10 +0,0 @@
package gitbucket.core.api
case class CreateARelease(
tag_name: String,
target_commitish: Option[String],
name: Option[String],
body: Option[String],
draft: Option[Boolean],
prerelease: Option[Boolean]
)

View File

@@ -5,15 +5,15 @@ package gitbucket.core.api
* api form * api form
*/ */
case class CreateARepository( case class CreateARepository(
name: String, name: String,
description: Option[String], description: Option[String],
`private`: Boolean = false, `private`: Boolean = false,
auto_init: Boolean = false auto_init: Boolean = false
) { ) {
def isValid: Boolean = { def isValid: Boolean = {
name.length <= 100 && name.length <= 100 &&
name.matches("[a-zA-Z0-9\\-\\+_.]+") && name.matches("[a-zA-Z0-9\\-\\+_.]+") &&
!name.startsWith("_") && !name.startsWith("_") &&
!name.startsWith("-") !name.startsWith("-")
} }
} }

View File

@@ -18,9 +18,9 @@ case class CreateAStatus(
) { ) {
def isValid: Boolean = { def isValid: Boolean = {
CommitState.valueOf(state).isDefined && CommitState.valueOf(state).isDefined &&
// only http // only http
target_url.forall(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length < 255) && target_url.forall(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length < 255) &&
context.forall(f => f.length < 255) && context.forall(f => f.length < 255) &&
description.forall(f => f.length < 1000) description.forall(f => f.length < 1000)
} }
} }

View File

@@ -1,11 +0,0 @@
package gitbucket.core.api
case class CreateAUser(
login: String,
password: String,
email: String,
fullName: Option[String],
isAdmin: Option[Boolean],
description: Option[String],
url: Option[String]
)

View File

@@ -1,12 +1,11 @@
package gitbucket.core.api package gitbucket.core.api
/** /**
* https://developer.github.com/v3/issues/#create-an-issue * https://developer.github.com/v3/issues/#create-an-issue
*/ */
case class CreateAnIssue( case class CreateAnIssue(
title: String, title: String,
body: Option[String], body: Option[String],
assignees: List[String], assignees: List[String],
milestone: Option[Int], milestone: Option[Int],
labels: List[String] labels: List[String])
)

View File

@@ -15,16 +15,12 @@ object JsonFormat {
val parserISO = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") val parserISO = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date]( val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
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)) }
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[ApiUser]() +
FieldSerializer[ApiGroup]() +
FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiPullRequest]() +
FieldSerializer[ApiRepository]() + FieldSerializer[ApiRepository]() +
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem.Parent]() +
@@ -43,35 +39,21 @@ object JsonFormat {
FieldSerializer[ApiCommits.Tree]() + FieldSerializer[ApiCommits.Tree]() +
FieldSerializer[ApiCommits.Stats]() + FieldSerializer[ApiCommits.Stats]() +
FieldSerializer[ApiCommits.File]() + FieldSerializer[ApiCommits.File]() +
FieldSerializer[ApiRelease]() +
FieldSerializer[ApiReleaseAsset]() +
ApiBranchProtection.enforcementLevelSerializer ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](_ => ({
new CustomSerializer[ApiPath]( case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
_ => case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
({ }, {
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length)) case ApiPath(path) => JString(c.baseUrl + path)
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath") }))
}, {
case ApiPath(path) => JString(c.baseUrl + path)
})
)
def sshPathSerializer(c: Context) = def sshPathSerializer(c: Context) = new CustomSerializer[SshPath](_ => ({
new CustomSerializer[SshPath]( case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) => SshPath(s.substring(c.sshUrl.get.length))
_ => case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
({ }, {
case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) => case SshPath(path) => c.sshUrl.map { sshUrl => JString(sshUrl + path) } getOrElse JNothing
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
})
)
/** /**
* convert object to json string * convert object to json string

View File

@@ -1,11 +0,0 @@
package gitbucket.core.api
case class UpdateAUser(
name: Option[String],
email: Option[String],
blog: Option[String],
company: Option[String],
location: Option[String],
hireable: Option[Boolean],
bio: Option[String]
)

View File

@@ -1,29 +1,50 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.controller.api._ import gitbucket.core.model._
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.PullRequestService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.scalatra.{Created, NoContent, UnprocessableEntity}
class ApiController import scala.collection.JavaConverters._
extends ApiControllerBase
with ApiGitReferenceControllerBase class ApiController extends ApiControllerBase
with ApiIssueCommentControllerBase with RepositoryService
with ApiIssueControllerBase with AccountService
with ApiIssueLabelControllerBase with ProtectedBranchService
with ApiOrganizationControllerBase with IssuesService
with ApiPullRequestControllerBase with LabelsService
with ApiReleaseControllerBase with MilestonesService
with ApiRepositoryBranchControllerBase with PullRequestService
with ApiRepositoryCollaboratorControllerBase with CommitsService
with ApiRepositoryCommitControllerBase with CommitStatusService
with ApiRepositoryContentsControllerBase with RepositoryCreationService
with ApiRepositoryControllerBase with IssueCreationService
with ApiRepositoryStatusControllerBase with HandleCommentService
with ApiUserControllerBase with WebHookService
with RepositoryService with WebHookPullRequestService
with WebHookIssueCommentService
with WikiService
with ActivityService
with PrioritiesService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator
trait ApiControllerBase extends ControllerBase {
self: RepositoryService
with AccountService with AccountService
with ProtectedBranchService with ProtectedBranchService
with IssuesService with IssuesService
@@ -32,69 +53,648 @@ class ApiController
with PullRequestService with PullRequestService
with CommitsService with CommitsService
with CommitStatusService with CommitStatusService
with ReleaseService
with RepositoryCreationService with RepositoryCreationService
with RepositoryCommitFileService
with IssueCreationService with IssueCreationService
with HandleCommentService with HandleCommentService
with MergeService
with WebHookService
with WebHookPullRequestService
with WebHookIssueCommentService
with WebHookPullRequestReviewCommentService
with WikiService
with ActivityService
with PrioritiesService with PrioritiesService
with AdminAuthenticator
with OwnerAuthenticator with OwnerAuthenticator
with UsersAuthenticator with UsersAuthenticator
with GroupManagerAuthenticator with GroupManagerAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with WritableUsersAuthenticator with WritableUsersAuthenticator =>
trait ApiControllerBase extends ControllerBase {
/** /**
* 404 for non-implemented api * 404 for non-implemented api
*/ */
get("/api/v3/*") { get("/api/v3/*") {
NotFound() NotFound()
} }
post("/api/v3/*") {
NotFound() /**
} * https://developer.github.com/v3/#root-endpoint
put("/api/v3/*") { */
NotFound() get("/api/v3/") {
} JsonFormat(ApiEndPoint())
delete("/api/v3/*") {
NotFound()
}
patch("/api/v3/*") {
NotFound()
} }
/** /**
* https://developer.github.com/v3/#root-endpoint * https://developer.github.com/v3/orgs/#get-an-organization
*/ */
get("/api/v3") { get("/api/v3/orgs/:groupName") {
JsonFormat(ApiEndPoint()) getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound()
} }
/**
* https://developer.github.com/v3/users/#get-a-single-user
* This API also returns group information (as GitHub).
*/
get("/api/v3/users/:userName") {
getAccountByUserName(params("userName")).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound()
}
/**
* https://developer.github.com/v3/repos/#list-organization-repositories
*/
get("/api/v3/orgs/:orgName/repos") {
JsonFormat(getVisibleRepositories(context.loginAccount, Some(params("orgName"))).map{ r => ApiRepository(r, getAccountByUserName(r.owner).get)})
}
/**
* https://developer.github.com/v3/repos/#list-user-repositories
*/
get("/api/v3/users/:userName/repos") {
JsonFormat(getVisibleRepositories(context.loginAccount, Some(params("userName"))).map{ r => ApiRepository(r, getAccountByUserName(r.owner).get)})
}
/*
* https://developer.github.com/v3/repos/branches/#list-branches
*/
get ("/api/v3/repos/:owner/:repo/branches")(referrersOnly { repository =>
JsonFormat(JGitUtil.getBranches(
owner = repository.owner,
name = repository.name,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
).map { br =>
ApiBranchForList(br.name, ApiBranchCommit(br.commitId))
})
})
/**
* https://developer.github.com/v3/repos/branches/#get-branch
*/
get ("/api/v3/repos/:owner/:repo/branches/*")(referrersOnly { repository =>
//import gitbucket.core.api._
(for{
branch <- params.get("splat") if repository.branchList.contains(branch)
br <- getBranches(repository.owner, repository.name, 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()
})
/*
* https://developer.github.com/v3/repos/contents/#get-contents
*/
get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository =>
def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = {
val path = new java.io.File(pathStr)
val dirName = path.getParent match {
case null => "."
case s => s
}
getFileList(git, revision, dirName).find(f => f.name.equals(path.getName))
}
val path = multiParams("splat").head match {
case s if s.isEmpty => "."
case s => s
}
val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git =>
val fileList = getFileList(git, refStr, path)
if (fileList.isEmpty) { // file or NotFound
getFileInfo(git, refStr, path).flatMap(f => {
val largeFile = params.get("large_file").exists(s => s.equals("true"))
val content = getContentFromId(git, f.id, largeFile)
request.getHeader("Accept") match {
case "application/vnd.github.v3.raw" => {
contentType = "application/vnd.github.v3.raw"
content
}
case "application/vnd.github.v3.html" if isRenderable(f.name) => {
contentType = "application/vnd.github.v3.html"
content.map(c =>
List(
"<div data-path=\"", path, "\" id=\"file\">", "<article>",
renderMarkup(path.split("/").toList, new String(c), refStr, repository, false, false, true).body,
"</article>", "</div>"
).mkString
)
}
case "application/vnd.github.v3.html" => {
contentType = "application/vnd.github.v3.html"
content.map(c =>
List(
"<div data-path=\"", path, "\" id=\"file\">", "<div class=\"plain\">", "<pre>",
play.twirl.api.HtmlFormat.escape(new String(c)).body,
"</pre>", "</div>", "</div>"
).mkString
)
}
case _ =>
Some(JsonFormat(ApiContents(f, RepositoryName(repository), content)))
}
}).getOrElse(NotFound())
} else { // directory
JsonFormat(fileList.map{f => ApiContents(f, RepositoryName(repository), None)})
}
}
})
/*
* https://developer.github.com/v3/git/refs/#get-a-reference
*/
get("/api/v3/repos/:owner/:repo/git/*") (referrersOnly { repository =>
val revstr = multiParams("splat").head
using(Git.open(getRepositoryDir(params("owner"), params("repo")))) { git =>
//JsonFormat( (revstr, git.getRepository().resolve(revstr)) )
// getRef is deprecated by jgit-4.2. use exactRef() or findRef()
val sha = git.getRepository().exactRef(revstr).getObjectId().name()
JsonFormat(ApiRef(revstr, ApiObject(sha)))
}
})
/**
* https://developer.github.com/v3/repos/collaborators/#list-collaborators
*/
get("/api/v3/repos/:owner/:repo/collaborators") (referrersOnly { repository =>
// TODO Should ApiUser take permission? getCollaboratorUserNames does not return owner group members.
JsonFormat(getCollaboratorUserNames(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get)))
})
/**
* https://developer.github.com/v3/users/#get-the-authenticated-user
*/
get("/api/v3/user") {
context.loginAccount.map { account =>
JsonFormat(ApiUser(account))
} getOrElse Unauthorized()
}
/**
* List user's own repository
* https://developer.github.com/v3/repos/#list-your-repositories
*/
get("/api/v3/user/repos")(usersOnly{
JsonFormat(getVisibleRepositories(context.loginAccount, Option(context.loginAccount.get.userName)).map{
r => ApiRepository(r, getAccountByUserName(r.owner).get)
})
})
/**
* Create user repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/user/repos")(usersOnly {
val owner = context.loginAccount.get.userName
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${owner}/${data.name}") {
if(getRepository(owner, data.name).isEmpty){
createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(owner, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
ApiError(
"A repository with this name already exists on this account",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound()
})
/**
* Create group repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/orgs/:org/repos")(managersOnly {
val groupName = params("org")
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${groupName}/${data.name}") {
if(getRepository(groupName, data.name).isEmpty){
createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(groupName, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else {
ApiError(
"A repository with this name already exists for this group",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
patch("/api/v3/repos/:owner/:repo/branches/*")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("splat") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
br <- getBranches(repository.owner, repository.name, 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()
})
/** /**
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status * @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
* but not enabled. * but not enabled.
*/ */
get("/api/v3/rate_limit") { get("/api/v3/rate_limit"){
contentType = formats("json") contentType = formats("json")
// this message is same as github enterprise... // this message is same as github enterprise...
org.scalatra.NotFound(ApiError("Rate limiting is not enabled.")) org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
} }
/** /**
* non-GitHub compatible API for listing plugins * https://developer.github.com/v3/issues/#list-issues-for-a-repository
*/
get("/api/v3/repos/:owner/:repository/issues")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account)] =
searchIssueByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
limit = PullRequestLimit,
repos = repository.owner -> repository.name
)
JsonFormat(issues.map { case (issue, issueUser) =>
ApiIssue(
issue = issue,
repositoryName = RepositoryName(repository),
user = ApiUser(issueUser)
)
})
})
/**
* https://developer.github.com/v3/issues/#get-a-single-issue
*/ */
get("/api/v3/gitbucket/plugins") { get("/api/v3/repos/:owner/:repository/issues/:id")(referrersOnly { repository =>
PluginRegistry().getPlugins().map { ApiPlugin(_) } (for{
issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString)
openedUser <- getAccountByUserName(issue.openedUserName)
} yield {
JsonFormat(ApiIssue(issue, RepositoryName(repository), ApiUser(openedUser)))
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/issues/#create-an-issue
*/
post("/api/v3/repos/:owner/:repository/issues")(readableUsersOnly { repository =>
if(isIssueEditable(repository)){ // TODO Should this check is provided by authenticator?
(for{
data <- extractFromJsonBody[CreateAnIssue]
loginAccount <- context.loginAccount
} yield {
val milestone = data.milestone.flatMap(getMilestone(repository.owner, repository.name, _))
val issue = createIssue(
repository,
data.title,
data.body,
data.assignees.headOption,
milestone.map(_.milestoneId),
None,
data.labels,
loginAccount)
JsonFormat(ApiIssue(issue, RepositoryName(repository), ApiUser(loginAccount)))
}) getOrElse NotFound()
} else Unauthorized()
})
/**
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
*/
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for{
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) })
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/issues/comments/#create-a-comment
*/
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString)
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
(issue, id) <- handleComment(issue, Some(body), repository, action)
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
}) getOrElse NotFound()
})
/**
* List all labels for this repository
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
*/
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
ApiLabel(label, RepositoryName(repository))
})
})
/**
* Get a single label
* https://developer.github.com/v3/issues/labels/#get-a-single-label
*/
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
JsonFormat(ApiLabel(label, RepositoryName(repository)))
} getOrElse NotFound()
})
/**
* Create a label
* https://developer.github.com/v3/issues/labels/#create-a-label
*/
post("/api/v3/repos/:owner/:repository/labels")(writableUsersOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
getLabel(repository.owner, repository.name, labelId).map { label =>
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
} getOrElse NotFound()
} 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()
})
/**
* Update a label
* https://developer.github.com/v3/issues/labels/#update-a-label
*/
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository =>
(for{
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)
))
} 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()
})
/**
* Delete a label
* https://developer.github.com/v3/issues/labels/#delete-a-label
*/
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository =>
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId)
NoContent()
} getOrElse NotFound()
}
})
/**
* https://developer.github.com/v3/pulls/#list-pull-requests
*/
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
searchPullRequestByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
limit = PullRequestLimit,
repos = repository.owner -> repository.name
)
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
})
/**
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set.empty)
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
JsonFormat(ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
))
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
*/
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(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)
}
}
} getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/#get
*/
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
})
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
*/
post("/api/v3/repos/:owner/:repo/statuses/:sha")(writableUsersOnly { repository =>
(for{
ref <- params.get("sha")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
data <- extractFromJsonBody[CreateAStatus] if data.isValid
creator <- context.loginAccount
state <- CommitState.valueOf(data.state)
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
state, data.target_url, data.description, new java.util.Date(), creator)
status <- getCommitStatus(repository.owner, repository.name, statusId)
} yield {
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
})
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* legacy route
*/
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
listStatusesRoute.action()
} }
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
owner <- getAccountByUserName(repository.owner)
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/commits/#get-a-single-commit
*/
get("/api/v3/repos/:owner/:repo/commits/:sha")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
val sha = params("sha")
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val objectId = repo.resolve(sha)
val commitInfo = using(new RevWalk(repo)){ revWalk =>
new CommitInfo(revWalk.parseCommit(objectId))
}
JsonFormat(ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.head, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
))
}
})
private def getAccount(userName: String, email: String): Account = {
getAccountByMailAddress(email).getOrElse {
Account(
userName = userName,
fullName = userName,
mailAddress = email,
password = "xxx",
isAdmin = false,
url = None,
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date(),
lastLoginDate = None,
image = None,
isGroupAccount = false,
isRemoved = true,
description = None
)
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
/**
* non-GitHub compatible API for Jenkins-Plugin
*/
get("/api/v3/repos/:owner/:repo/raw/*")(referrersOnly { repository =>
val (id, path) = repository.splitPath(multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).map { objectId =>
responseRawFile(git, objectId, path, repository)
} getOrElse NotFound()
}
})
} }

View File

@@ -1,10 +1,10 @@
package gitbucket.core.controller package gitbucket.core.controller
import java.io.{File, FileInputStream} import java.io.FileInputStream
import gitbucket.core.api.{ApiError, JsonFormat} import gitbucket.core.api.ApiError
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService} import gitbucket.core.service.{AccountService, SystemSettingsService,RepositoryService}
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
@@ -17,11 +17,9 @@ import org.scalatra.forms._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import javax.servlet.{FilterChain, ServletRequest, ServletResponse} import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
import is.tagomor.woothee.Classifier
import scala.util.Try import scala.util.Try
import scala.util.Using
import net.coobird.thumbnailator.Thumbnails import net.coobird.thumbnailator.Thumbnails
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
@@ -32,14 +30,9 @@ import org.slf4j.LoggerFactory
/** /**
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase abstract class ControllerBase extends ScalatraFilter
extends ScalatraFilter with ValidationSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with ValidationSupport with SystemSettingsService {
with JacksonJsonSupport
with I18nSupport
with FlashMapSupport
with Validations
with SystemSettingsService {
private val logger = LoggerFactory.getLogger(getClass) private val logger = LoggerFactory.getLogger(getClass)
@@ -47,34 +40,40 @@ abstract class ControllerBase
before("/api/v3/*") { before("/api/v3/*") {
contentType = formats("json") contentType = formats("json")
request.setAttribute(Keys.Request.APIv3, true)
} }
override def requestPath(uri: String, idx: Int): String = { override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
val path = super.requestPath(uri, idx) val httpRequest = request.asInstanceOf[HttpServletRequest]
if (path != "/" && path.endsWith("/")) { val httpResponse = response.asInstanceOf[HttpServletResponse]
path.substring(0, path.length - 1) val context = request.getServletContext.getContextPath
} else { val path = httpRequest.getRequestURI.substring(context.length)
path
}
}
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = if(path.startsWith("/console/")){
try { val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val httpRequest = request.asInstanceOf[HttpServletRequest] val baseUrl = this.baseUrl(httpRequest)
val context = request.getServletContext.getContextPath if(account == null){
val path = httpRequest.getRequestURI.substring(context.length) // Redirect to login form
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
if (path.startsWith("/git/") || path.startsWith("/git-lfs/")) { } else if(account.isAdmin){
// Git repository // H2 Console (administrators only)
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
// Scalatra actions // Redirect to dashboard
super.doFilter(request, response, chain) httpResponse.sendRedirect(baseUrl + "/")
} }
} finally { } else if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
contextCache.remove() // Git repository
chain.doFilter(request, response)
} else {
if(path.startsWith("/api/v3/")){
httpRequest.setAttribute(Keys.Request.APIv3, true)
}
// Scalatra actions
super.doFilter(request, response, chain)
} }
} finally {
contextCache.remove();
}
private val contextCache = new java.lang.ThreadLocal[Context]() private val contextCache = new java.lang.ThreadLocal[Context]()
@@ -92,130 +91,102 @@ abstract class ControllerBase
} }
} }
private def LoginAccount: Option[Account] = private def LoginAccount: Option[Account] = request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
def ajaxGet(path: String)(action: => Any): Route = def ajaxGet(path : String)(action : => Any) : Route =
super.get(path) { super.get(path){
request.setAttribute(Keys.Request.Ajax, "true") request.setAttribute(Keys.Request.Ajax, "true")
action action
} }
override def ajaxGet[T](path: String, form: ValueType[T])(action: T => Any): Route = override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route =
super.ajaxGet(path, form) { form => super.ajaxGet(path, form){ form =>
request.setAttribute(Keys.Request.Ajax, "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
def ajaxPost(path: String)(action: => Any): Route = def ajaxPost(path : String)(action : => Any) : Route =
super.post(path) { super.post(path){
request.setAttribute(Keys.Request.Ajax, "true") request.setAttribute(Keys.Request.Ajax, "true")
action action
} }
override def ajaxPost[T](path: String, form: ValueType[T])(action: T => Any): Route = override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route =
super.ajaxPost(path, form) { form => super.ajaxPost(path, form){ form =>
request.setAttribute(Keys.Request.Ajax, "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
protected def NotFound() = protected def NotFound() =
if (request.hasAttribute(Keys.Request.Ajax)) { if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound() org.scalatra.NotFound()
} else if (request.hasAttribute(Keys.Request.APIv3)) { } else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json") contentType = formats("json")
org.scalatra.NotFound(JsonFormat(ApiError("Not Found"))) org.scalatra.NotFound(ApiError("Not Found"))
} else { } else {
org.scalatra.NotFound(gitbucket.core.html.error("Not Found")) org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
} }
private def isBrowser(userAgent: String): Boolean = {
if (userAgent == null || userAgent.isEmpty) {
false
} else {
val data = Classifier.parse(userAgent)
val category = data.get("category")
category == "pc" || category == "smartphone" || category == "mobilephone"
}
}
protected def Unauthorized()(implicit context: Context) = protected def Unauthorized()(implicit context: Context) =
if (request.hasAttribute(Keys.Request.Ajax)) { if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized() org.scalatra.Unauthorized()
} else if (request.hasAttribute(Keys.Request.APIv3)) { } else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json") contentType = formats("json")
org.scalatra.Unauthorized(JsonFormat(ApiError("Requires authentication"))) org.scalatra.Unauthorized(ApiError("Requires authentication"))
} else if (!isBrowser(request.getHeader("USER-AGENT"))) {
org.scalatra.Unauthorized()
} else { } else {
if (context.loginAccount.isDefined) { if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/")) org.scalatra.Unauthorized(redirect("/"))
} else { } else {
if (request.getMethod.toUpperCase == "POST") { if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin")) org.scalatra.Unauthorized(redirect("/signin"))
} else { } else {
org.scalatra.Unauthorized( org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
redirect( defining(request.getQueryString){ queryString =>
"/signin?redirect=" + StringUtil.urlEncode( request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
defining(request.getQueryString) { queryString => }
request.getRequestURI.substring(request.getContextPath.length) + (if (queryString != null) )))
"?" + queryString
else "")
}
)
)
)
} }
} }
} }
error { error{
case e => { case e => {
logger.error(s"Catch unhandled error in request: ${request}", e) logger.error(s"Catch unhandled error in request: ${request}", e)
if (request.hasAttribute(Keys.Request.Ajax)) { if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.InternalServerError() org.scalatra.InternalServerError()
} else if (request.hasAttribute(Keys.Request.APIv3)) { } else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json") contentType = formats("json")
org.scalatra.InternalServerError(JsonFormat(ApiError("Internal Server Error"))) org.scalatra.InternalServerError(ApiError("Internal Server Error"))
} else { } else {
org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e))) org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e)))
} }
} }
} }
override def url( override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
path: String, includeContextPath: Boolean = true, includeServletPath: Boolean = true,
params: Iterable[(String, Any)] = Iterable.empty, absolutize: Boolean = true, withSessionId: Boolean = true)
includeContextPath: Boolean = true, (implicit request: HttpServletRequest, response: HttpServletResponse): String =
includeServletPath: Boolean = true,
absolutize: Boolean = true,
withSessionId: Boolean = true
)(implicit request: HttpServletRequest, response: HttpServletResponse): String =
if (path.startsWith("http")) path if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false) else baseUrl + super.url(path, params, false, false, false)
/** /**
* Extends scalatra-form's trim rule to eliminate CR and LF. * Extends scalatra-form's trim rule to eliminate CR and LF.
*/ */
protected def trim2[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T]() { protected def trim2[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){
def convert(value: String, messages: Messages): T = valueType.convert(trim(value), messages) def convert(value: String, messages: Messages): T = valueType.convert(trim(value), messages)
override def validate( override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
name: String,
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Seq[(String, String)] =
valueType.validate(name, trim(value), params, messages) valueType.validate(name, trim(value), params, messages)
private def trim(value: String): String = if (value == null) null else value.replace("\r\n", "").trim private def trim(value: String): String = if(value == null) null else value.replace("\r\n", "").trim
} }
/** /**
* Use this method to response the raw data against XSS. * Use this method to response the raw data against XSS.
*/ */
protected def RawData[T](contentType: String, rawData: T): T = { protected def RawData[T](contentType: String, rawData: T): T = {
if (contentType.split(";").head.trim.toLowerCase.startsWith("text/html")) { if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){
this.contentType = "text/plain" this.contentType = "text/plain"
} else { } else {
this.contentType = contentType this.contentType = contentType
@@ -225,39 +196,35 @@ abstract class ControllerBase
} }
// jenkins send message as 'application/x-www-form-urlencoded' but scalatra already parsed as multi-part-request. // jenkins send message as 'application/x-www-form-urlencoded' but scalatra already parsed as multi-part-request.
def extractFromJsonBody[A](implicit request: HttpServletRequest, mf: Manifest[A]): Option[A] = { def extractFromJsonBody[A](implicit request:HttpServletRequest, mf:Manifest[A]): Option[A] = {
(request.contentType.map(_.split(";").head.toLowerCase) match { (request.contentType.map(_.split(";").head.toLowerCase) match{
case Some("application/x-www-form-urlencoded") => multiParams.keys.headOption.map(parse(_)) case Some("application/x-www-form-urlencoded") => multiParams.keys.headOption.map(parse(_))
case Some("application/json") => Some(parsedBody) case Some("application/json") => Some(parsedBody)
case _ => Some(parse(request.body)) case _ => Some(parse(request.body))
}).filterNot(_ == JNothing).flatMap(j => Try(j.extract[A]).toOption) }).filterNot(_ == JNothing).flatMap(j => Try(j.extract[A]).toOption)
} }
protected def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = { protected def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@scala.annotation.tailrec @scala.annotation.tailrec
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if (walk.getPathString == path) => Some(walk.getObjectId(0)) case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk) case true => _getPathObjectId(path, walk)
case false => None case false => None
} }
Using.resource(new TreeWalk(git.getRepository)) { treeWalk => using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true) treeWalk.setRecursive(true)
_getPathObjectId(path, treeWalk) _getPathObjectId(path, treeWalk)
} }
} }
protected def responseRawFile( protected def responseRawFile(git: Git, objectId: ObjectId, path: String,
git: Git, repository: RepositoryService.RepositoryInfo): Unit = {
objectId: ObjectId, JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
path: String, contentType = FileUtil.getMimeType(path)
repository: RepositoryService.RepositoryInfo
): Unit = {
JGitUtil.getObjectLoaderFromId(git, objectId) { loader =>
contentType = FileUtil.getSafeMimeType(path)
if (loader.isLarge) { if(loader.isLarge){
response.setContentLength(loader.getSize.toInt) response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream) loader.copyTo(response.outputStream)
} else { } else {
@@ -265,11 +232,11 @@ abstract class ControllerBase
val text = new String(bytes, "UTF-8") val text = new String(bytes, "UTF-8")
val attrs = JGitUtil.getLfsObjects(text) val attrs = JGitUtil.getLfsObjects(text)
if (attrs.nonEmpty) { if(attrs.nonEmpty) {
response.setContentLength(attrs("size").toInt) response.setContentLength(attrs("size").toInt)
val oid = attrs("oid").split(":")(1) val oid = attrs("oid").split(":")(1)
Using.resource(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))) { in => using(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))){ in =>
IOUtils.copy(in, response.getOutputStream) IOUtils.copy(in, response.getOutputStream)
} }
} else { } else {
@@ -284,21 +251,17 @@ abstract class ControllerBase
/** /**
* Context object for the current request. * Context object for the current request.
*/ */
case class Context( case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
settings: SystemSettingsService.SystemSettings,
loginAccount: Option[Account],
request: HttpServletRequest
) {
val path = settings.baseUrl.getOrElse(request.getContextPath) val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath = request.getRequestURI.substring(request.getContextPath.length) val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request) val baseUrl = settings.baseUrl(request)
val host = new java.net.URL(baseUrl).getHost val host = new java.net.URL(baseUrl).getHost
val platform = request.getHeader("User-Agent") match { val platform = request.getHeader("User-Agent") match {
case null => null case null => null
case agent if agent.contains("Mac") => "mac" case agent if agent.contains("Mac") => "mac"
case agent if agent.contains("Linux") => "linux" case agent if agent.contains("Linux") => "linux"
case agent if agent.contains("Win") => "windows" case agent if agent.contains("Win") => "windows"
case _ => null case _ => null
} }
val sidebarCollapse = request.getSession.getAttribute("sidebar-collapse") != null val sidebarCollapse = request.getSession.getAttribute("sidebar-collapse") != null
@@ -309,7 +272,7 @@ case class Context(
* Cached object are available during a request. * Cached object are available during a request.
*/ */
def cache[A](key: String)(action: => A): A = def cache[A](key: String)(action: => A): A =
defining(Keys.Request.Cache(key)) { cacheKey => defining(Keys.Request.Cache(key)){ cacheKey =>
Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse { Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
val newObject = action val newObject = action
request.setAttribute(cacheKey, newObject) request.setAttribute(cacheKey, newObject)
@@ -323,111 +286,48 @@ case class Context(
* Base trait for controllers which manages account information. * Base trait for controllers which manages account information.
*/ */
trait AccountManagementControllerBase extends ControllerBase { trait AccountManagementControllerBase extends ControllerBase {
self: AccountService => self: AccountService =>
private val logger = LoggerFactory.getLogger(getClass)
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
if (clearImage) { if(clearImage){
getAccountByUserName(userName).flatMap(_.image).foreach { image => getAccountByUserName(userName).flatMap(_.image).map { image =>
new File(getUserUploadDir(userName), FileUtil.checkFilename(image)).delete() new java.io.File(getUserUploadDir(userName), image).delete()
updateAvatarImage(userName, None) updateAvatarImage(userName, None)
} }
} else { } else {
try { fileId.map { fileId =>
fileId.foreach { fileId => val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get) val uploadDir = getUserUploadDir(userName)
val uploadDir = getUserUploadDir(userName) if(!uploadDir.exists){
if (!uploadDir.exists) { uploadDir.mkdirs()
uploadDir.mkdirs()
}
Thumbnails
.of(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(fileId)))
.size(324, 324)
.toFile(new File(uploadDir, FileUtil.checkFilename(filename)))
updateAvatarImage(userName, Some(filename))
} }
} catch { Thumbnails.of(new java.io.File(getTemporaryDir(session.getId), fileId))
case e: Exception => logger.info("Error while updateImage" + e.getMessage) .size(324, 324)
.toFile(new java.io.File(uploadDir, filename))
updateAvatarImage(userName, Some(filename))
} }
} }
protected def uniqueUserName: Constraint = new Constraint() { protected def uniqueUserName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserNameIgnoreCase(value, true).map { _ => getAccountByUserName(value, true).map { _ => "User already exists." }
"User already exists."
}
} }
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint() { protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
override def validate( override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
name: String, getAccountByMailAddress(value, true)
value: String, .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.optionValue(paramName) }
params: Map[String, Seq[String]], .map { _ => "Mail address is already registered." }
messages: Messages
): Option[String] = {
val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses"))
if (extraMailAddresses.exists {
case (k, v) =>
v.contains(value)
}) {
Some("These mail addresses are duplicated.")
} else {
getAccountByMailAddress(value, true)
.collect {
case x if paramName.isEmpty || Some(x.userName) != params.optionValue(paramName) =>
"Mail address is already registered."
}
}
} }
} }
protected def uniqueExtraMailAddress(paramName: String = ""): Constraint = new Constraint() { val allReservedNames = Set("git", "admin", "upload", "api", "assets", "plugin-assets", "signin", "signout", "register", "activities.atom", "sidebar-collapse", "groups", "new")
override def validate( protected def reservedNames(): Constraint = new Constraint(){
name: String, override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){
value: String, Some(s"${value} is reserved")
params: Map[String, Seq[String]], } else {
messages: Messages None
): Option[String] = {
val extraMailAddresses = params.view.filterKeys(k => k.startsWith("extraMailAddresses"))
if (Some(value) == params.optionValue("mailAddress") || extraMailAddresses.count {
case (k, v) =>
v.contains(value)
} > 1) {
Some("These mail addresses are duplicated.")
} else {
getAccountByMailAddress(value, true)
.collect {
case x if paramName.isEmpty || Some(x.userName) != params.optionValue(paramName) =>
"Mail address is already registered."
}
}
} }
} }
val allReservedNames = Set(
"git",
"admin",
"upload",
"api",
"assets",
"plugin-assets",
"signin",
"signout",
"register",
"activities.atom",
"sidebar-collapse",
"groups",
"new"
)
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")
} else {
None
}
}
} }

View File

@@ -6,35 +6,13 @@ import gitbucket.core.util.{Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.service.IssuesService._ import gitbucket.core.service.IssuesService._
class DashboardController class DashboardController extends DashboardControllerBase
extends DashboardControllerBase with IssuesService with PullRequestService with RepositoryService with AccountService with CommitsService
with IssuesService with UsersAuthenticator
with MergeService
with PullRequestService
with RepositoryService
with AccountService
with ActivityService
with CommitsService
with LabelsService
with PrioritiesService
with WebHookService
with WebHookPullRequestService
with WebHookPullRequestReviewCommentService
with MilestonesService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase { trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with AccountService with UsersAuthenticator => self: IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator =>
get("/dashboard/repos")(usersOnly {
val repos = getVisibleRepositories(
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
)
html.repos(getGroupNames(context.loginAccount.get.userName), repos, repos)
})
get("/dashboard/issues")(usersOnly { get("/dashboard/issues")(usersOnly {
searchIssues("created_by") searchIssues("created_by")
@@ -81,60 +59,51 @@ trait DashboardControllerBase extends ControllerBase {
private def searchIssues(filter: String) = { private def searchIssues(filter: String) = {
import IssuesService._ import IssuesService._
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name) val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
html.issues( html.issues(
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page, page,
countIssue(condition.copy(state = "open"), false, userRepos: _*), countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), false, userRepos: _*), countIssue(condition.copy(state = "closed"), false, userRepos: _*),
filter match { filter match {
case "assigned" => condition.copy(assigned = Some(Some(userName))) case "assigned" => condition.copy(assigned = Some(Some(userName)))
case "mentioned" => condition.copy(mentioned = Some(userName)) case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName)) case _ => condition.copy(author = Some(userName))
}, },
filter, filter,
getGroupNames(userName), getGroupNames(userName),
getVisibleRepositories( Nil,
context.loginAccount, getUserRepositories(userName, withoutPhysicalInfo = true))
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
)
)
} }
private def searchPullRequests(filter: String) = { private def searchPullRequests(filter: String) = {
import IssuesService._ import IssuesService._
import PullRequestService._ import PullRequestService._
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName) val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
val allRepos = getAllRepositories(userName) val allRepos = getAllRepositories(userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
html.pulls( html.pulls(
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page, page,
countIssue(condition.copy(state = "open"), true, allRepos: _*), countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), true, allRepos: _*), countIssue(condition.copy(state = "closed"), true, allRepos: _*),
filter match { filter match {
case "assigned" => condition.copy(assigned = Some(Some(userName))) case "assigned" => condition.copy(assigned = Some(Some(userName)))
case "mentioned" => condition.copy(mentioned = Some(userName)) case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName)) case _ => condition.copy(author = Some(userName))
}, },
filter, filter,
getGroupNames(userName), getGroupNames(userName),
getVisibleRepositories( Nil,
context.loginAccount, getUserRepositories(userName, withoutPhysicalInfo = true))
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
)
)
} }
} }

View File

@@ -1,9 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import java.io.File
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, ReleaseService, RepositoryService} import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.servlet.Database import gitbucket.core.servlet.Database
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
@@ -16,190 +14,112 @@ import org.scalatra._
import org.scalatra.servlet.{FileItem, FileUploadSupport, MultipartConfig} import org.scalatra.servlet.{FileItem, FileUploadSupport, MultipartConfig}
import org.apache.commons.io.{FileUtils, IOUtils} import org.apache.commons.io.{FileUtils, IOUtils}
import scala.util.Using
import gitbucket.core.service.SystemSettingsService
/** /**
* Provides Ajax based file upload functionality. * Provides Ajax based file upload functionality.
* *
* This servlet saves uploaded file. * This servlet saves uploaded file.
*/ */
class FileUploadController class FileUploadController extends ScalatraServlet with FileUploadSupport with RepositoryService with AccountService {
extends ScalatraServlet
with FileUploadSupport
with RepositoryService
with AccountService
with ReleaseService
with SystemSettingsService {
post("/image") { val maxFileSize = if (System.getProperty("gitbucket.maxFileSize") != null)
setMultipartConfig() System.getProperty("gitbucket.maxFileSize").toLong
execute( else
{ (file, fileId) => 3 * 1024 * 1024
FileUtils
.writeByteArrayToFile(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(fileId)), file.get) configureMultipartHandling(MultipartConfig(maxFileSize = Some(maxFileSize)))
session += Keys.Session.Upload(fileId) -> file.name
}, post("/image"){
FileUtil.isImage execute({ (file, fileId) =>
) FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name
}, FileUtil.isImage)
} }
post("/tmp") { post("/tmp"){
setMultipartConfig() execute({ (file, fileId) =>
execute( FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
{ (file, fileId) => session += Keys.Session.Upload(fileId) -> file.name
FileUtils }, _ => true)
.writeByteArrayToFile(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(fileId)), file.get)
session += Keys.Session.Upload(fileId) -> file.name
},
_ => true
)
} }
post("/file/:owner/:repository") { post("/file/:owner/:repository"){
setMultipartConfig() execute({ (file, fileId) =>
execute( FileUtils.writeByteArrayToFile(new java.io.File(
{ (file, fileId) => getAttachedDir(params("owner"), params("repository")),
FileUtils.writeByteArrayToFile( fileId + "." + FileUtil.getExtension(file.getName)), file.get)
new File( }, _ => true)
getAttachedDir(params("owner"), params("repository")),
FileUtil.checkFilename(fileId + "." + FileUtil.getExtension(file.getName))
),
file.get
)
},
_ => true
)
} }
post("/wiki/:owner/:repository") { post("/wiki/:owner/:repository"){
setMultipartConfig()
// Don't accept not logged-in users // Don't accept not logged-in users
session.get(Keys.Session.LoginAccount).collect { session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account =>
case loginAccount: Account => val owner = params("owner")
val owner = params("owner") val repository = params("repository")
val repository = params("repository")
// Check whether logged-in user is collaborator // Check whether logged-in user is collaborator
onlyWikiEditable(owner, repository, loginAccount) { onlyWikiEditable(owner, repository, loginAccount){
execute( execute({ (file, fileId) =>
{ (file, fileId) => val fileName = file.getName
val fileName = file.getName LockUtil.lock(s"${owner}/${repository}/wiki") {
LockUtil.lock(s"${owner}/${repository}/wiki") { using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { val builder = DirCache.newInCore.builder()
git => val inserter = git.getRepository.newObjectInserter()
val builder = DirCache.newInCore.builder() val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
if (headId != null) { if(headId != null){
JGitUtil.processTree(git, headId) { (path, tree) => JGitUtil.processTree(git, headId){ (path, tree) =>
if (path != fileName) { if(path != fileName){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) 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
} }
} }
},
_ => true
)
}
} getOrElse BadRequest()
}
post("/release/:owner/:repository/:tag") { val bytes = IOUtils.toByteArray(file.getInputStream)
setMultipartConfigForLargeFile() builder.add(JGitUtil.createDirCacheEntry(fileName, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes)))
session builder.finish()
.get(Keys.Session.LoginAccount)
.collect { val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
case _: Account => Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, s"Uploaded ${fileName}")
val owner = params("owner")
val repository = params("repository") fileName
val tag = params("tag") }
execute( }
{ (file, fileId) => }, _ => true)
FileUtils.writeByteArrayToFile(
new File(getReleaseFilesDir(owner, repository), FileUtil.checkFilename(tag + "/" + fileId)),
file.get
)
},
_ => true
)
} }
.getOrElse(BadRequest()) } getOrElse BadRequest()
} }
post("/import") { post("/import") {
import JDBCUtil._ import JDBCUtil._
setMultipartConfig() session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
session.get(Keys.Session.LoginAccount).collect { execute({ (file, fileId) =>
case loginAccount: Account if loginAccount.isAdmin => request2Session(request).conn.importAsSQL(file.getInputStream)
execute({ (file, fileId) => }, _ => true)
request2Session(request).conn.importAsSQL(file.getInputStream)
}, _ => true)
} }
redirect("/admin/data") redirect("/admin/data")
} }
private def setMultipartConfig(): Unit = {
val settings = loadSystemSettings()
val config = MultipartConfig(maxFileSize = Some(settings.upload.maxFileSize))
config.apply(request.getServletContext())
}
private def setMultipartConfigForLargeFile(): Unit = {
val settings = loadSystemSettings()
val config = MultipartConfig(maxFileSize = Some(settings.upload.largeMaxFileSize))
config.apply(request.getServletContext())
}
private def onlyWikiEditable(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = { private def onlyWikiEditable(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = {
implicit val session = Database.getSession(request) implicit val session = Database.getSession(request)
getRepository(owner, repository) match { getRepository(owner, repository) match {
case Some(x) => case Some(x) => x.repository.options.wikiOption match {
x.repository.options.wikiOption match { case "ALL" if !x.repository.isPrivate => action
case "ALL" if !x.repository.isPrivate => action case "PUBLIC" if hasGuestRole(owner, repository, Some(loginAccount)) => action
case "PUBLIC" if hasGuestRole(owner, repository, Some(loginAccount)) => action case "PRIVATE" if hasDeveloperRole(owner, repository, Some(loginAccount)) => action
case "PRIVATE" if hasDeveloperRole(owner, repository, Some(loginAccount)) => action case _ => BadRequest()
case _ => BadRequest() }
}
case None => BadRequest() case None => BadRequest()
} }
} }
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = private def execute(f: (FileItem, String) => Unit , mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
fileParams.get("file") match { case Some(file) if(mimeTypeChcker(file.name)) =>
case Some(file) if (mimeTypeChcker(file.name)) => defining(FileUtil.generateFileId){ fileId =>
defining(FileUtil.generateFileId) { fileId => f(file, fileId)
f(file, fileId) contentType = "text/plain"
contentType = "text/plain" Ok(fileId)
Ok(fileId) }
} case _ => BadRequest()
case _ => BadRequest() }
}
} }

View File

@@ -1,45 +1,23 @@
package gitbucket.core.controller package gitbucket.core.controller
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.helper.xml
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util._ import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator}
import gitbucket.core.view.helpers._
import org.scalatra.Ok
import org.scalatra.forms._ import org.scalatra.forms._
import org.scalatra.Ok
class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
with UsersAuthenticator with ReferrerAuthenticator
class IndexController
extends IndexControllerBase
with RepositoryService
with ActivityService
with AccountService
with RepositorySearchService
with IssuesService
with LabelsService
with MilestonesService
with PrioritiesService
with UsersAuthenticator
with ReferrerAuthenticator
with AccessTokenService
with AccountFederationService
with OpenIDConnectService
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with ActivityService with UsersAuthenticator with ReferrerAuthenticator =>
with AccountService
with RepositorySearchService
with UsersAuthenticator
with ReferrerAuthenticator
with AccessTokenService
with AccountFederationService
with OpenIDConnectService =>
case class SignInForm(userName: String, password: String, hash: Option[String]) case class SignInForm(userName: String, password: String, hash: Option[String])
@@ -57,117 +35,50 @@ trait IndexControllerBase extends ControllerBase {
// //
// case class SearchForm(query: String, owner: String, repository: String) // case class SearchForm(query: String, owner: String, repository: String)
case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String)
get("/") { get("/"){
context.loginAccount context.loginAccount.map { account =>
.map { account => val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName) gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet), Nil, getUserRepositories(account.userName, withoutPhysicalInfo = true))
gitbucket.core.html.index( }.getOrElse {
getRecentActivitiesByOwners(visibleOwnerSet), gitbucket.core.html.index(getRecentActivities(), getVisibleRepositories(None, withoutPhysicalInfo = true), Nil)
getVisibleRepositories( }
Some(account),
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName
)
)
}
.getOrElse {
gitbucket.core.html.index(
getRecentActivities(),
getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false
)
}
} }
get("/signin") { get("/signin"){
val redirect = params.get("redirect") val redirect = params.get("redirect")
if (redirect.isDefined && redirect.get.startsWith("/")) { if(redirect.isDefined && redirect.get.startsWith("/")){
flash.update(Keys.Flash.Redirect, redirect.get) flash += Keys.Flash.Redirect -> redirect.get
} }
gitbucket.core.html.signin(flash.get("userName"), flash.get("password"), flash.get("error")) gitbucket.core.html.signin(flash.get("userName"), flash.get("password"), flash.get("error"))
} }
post("/signin", signinForm) { form => post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match { authenticate(context.settings, form.userName, form.password) match {
case Some(account) => case Some(account) => signin(account, form.hash)
flash.get(Keys.Flash.Redirect) match { case None => {
case Some(redirectUrl: String) => signin(account, redirectUrl + form.hash.getOrElse("")) flash += "userName" -> form.userName
case _ => signin(account) flash += "password" -> form.password
} flash += "error" -> "Sorry, your Username and/or Password is incorrect. Please try again."
case None =>
flash.update("userName", form.userName)
flash.update("password", form.password)
flash.update("error", "Sorry, your Username and/or Password is incorrect. Please try again.")
redirect("/signin") redirect("/signin")
}
}
/**
* Initiate an OpenID Connect authentication request.
*/
post("/signin/oidc") {
context.settings.oidc.map { oidc =>
val redirectURI = new URI(s"$baseUrl/signin/oidc")
val authenticationRequest = createOIDCAuthenticationRequest(oidc.issuer, oidc.clientID, redirectURI)
val redirectBackURI = flash.get(Keys.Flash.Redirect) match {
case Some(redirectBackURI: String) => redirectBackURI + params.getOrElse("hash", "")
case _ => "/"
} }
session.setAttribute(
Keys.Session.OidcContext,
OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
)
redirect(authenticationRequest.toURI.toString)
} getOrElse {
NotFound()
} }
} }
/** get("/signout"){
* Handle an OpenID Connect authentication response.
*/
get("/signin/oidc") {
context.settings.oidc.map { oidc =>
val redirectURI = new URI(s"$baseUrl/signin/oidc")
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.")
session.invalidate()
redirect("/signin")
}
case _ =>
flash.update("error", "Sorry, something wrong. Please try again.")
session.invalidate()
redirect("/signin")
}
} getOrElse {
NotFound()
}
}
get("/signout") {
session.invalidate session.invalidate
redirect("/") redirect("/")
} }
get("/activities.atom") { get("/activities.atom"){
contentType = "application/atom+xml; type=feed" contentType = "application/atom+xml; type=feed"
xml.feed(getRecentActivities()) xml.feed(getRecentActivities())
} }
post("/sidebar-collapse") { post("/sidebar-collapse"){
if (params("collapse") == "true") { if(params("collapse") == "true"){
session.setAttribute("sidebar-collapse", "true") session.setAttribute("sidebar-collapse", "true")
} else { } else {
session.setAttribute("sidebar-collapse", null) session.setAttribute("sidebar-collapse", null)
} }
Ok() Ok()
@@ -176,18 +87,22 @@ trait IndexControllerBase extends ControllerBase {
/** /**
* Set account information into HttpSession and redirect. * Set account information into HttpSession and redirect.
*/ */
private def signin(account: Account, redirectUrl: String = "/") = { private def signin(account: Account, hash: Option[String]) = {
session.setAttribute(Keys.Session.LoginAccount, account) session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName) updateLastLoginDate(account.userName)
if (LDAPUtil.isDummyMailAddress(account)) { if(LDAPUtil.isDummyMailAddress(account)) {
redirect("/" + account.userName + "/_edit") redirect("/" + account.userName + "/_edit")
} }
if (redirectUrl.stripSuffix("/") == request.getContextPath) { flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl + hash.getOrElse(""))
}
}.getOrElse {
redirect("/") redirect("/")
} else {
redirect(redirectUrl)
} }
} }
@@ -196,29 +111,23 @@ trait IndexControllerBase extends ControllerBase {
*/ */
get("/_user/proposals")(usersOnly { get("/_user/proposals")(usersOnly {
contentType = formats("json") contentType = formats("json")
val user = params("user").toBoolean val user = params("user").toBoolean
val group = params("group").toBoolean val group = params("group").toBoolean
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map( Map("options" -> (
"options" -> ( getAllUsers(false)
getAllUsers(false) .withFilter { t => (user, group) match {
.withFilter { t => case (true, true) => true
(user, group) match { case (true, false) => !t.isGroupAccount
case (true, true) => true case (false, true) => t.isGroupAccount
case (true, false) => !t.isGroupAccount case (false, false) => false
case (false, true) => t.isGroupAccount }}.map { t =>
case (false, false) => false Map(
} "label" -> s"<b>@${t.userName}</b> ${t.fullName}",
} "value" -> t.userName
.map { t => )
Map( }
"label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml(t.userName)}</b> ${StringUtil ))
.escapeHtml(t.fullName)}",
"value" -> t.userName
)
}
)
)
) )
}) })
@@ -227,85 +136,48 @@ trait IndexControllerBase extends ControllerBase {
* Returns a single string which is any of "group", "user" or "". * Returns a single string which is any of "group", "user" or "".
*/ */
post("/_user/existence")(usersOnly { post("/_user/existence")(usersOnly {
getAccountByUserNameIgnoreCase(params("userName")).map { account => getAccountByUserName(params("userName")).map { account =>
if (account.isGroupAccount) "group" else "user" if(account.isGroupAccount) "group" else "user"
} getOrElse "" } getOrElse ""
}) })
// TODO Move to RepositoryViewrController? // TODO Move to RepositoryViwerController?
get("/:owner/:repository/search")(referrersOnly { repository => get("/:owner/:repository/search")(referrersOnly { repository =>
defining(params.getOrElse("q", "").trim, params.getOrElse("type", "code")) { defining(params.getOrElse("q", "").trim, params.getOrElse("type", "code")){ case (query, target) =>
case (query, target) => val page = try {
val page = try { val i = params.getOrElse("page", "1").toInt
val i = params.getOrElse("page", "1").toInt if(i <= 0) 1 else i
if (i <= 0) 1 else i } catch {
} catch { case e: NumberFormatException => 1
case e: NumberFormatException => 1 }
}
target.toLowerCase match { target.toLowerCase match {
case "issues" => case "issue" => gitbucket.core.search.html.issues(
gitbucket.core.search.html.issues( if(query.nonEmpty) searchIssues(repository.owner, repository.name, query) else Nil,
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, false) else Nil, query, page, repository)
false,
query,
page,
repository
)
case "pulls" => case "wiki" => gitbucket.core.search.html.wiki(
gitbucket.core.search.html.issues( if(query.nonEmpty) searchWikiPages(repository.owner, repository.name, query) else Nil,
if (query.nonEmpty) searchIssues(repository.owner, repository.name, query, true) else Nil, query, page, repository)
true,
query,
page,
repository
)
case "wiki" => case _ => gitbucket.core.search.html.code(
gitbucket.core.search.html.wiki( if(query.nonEmpty) searchFiles(repository.owner, repository.name, query) else Nil,
if (query.nonEmpty) searchWikiPages(repository.owner, repository.name, query) else Nil, query, page, repository)
query, }
page,
repository
)
case _ =>
gitbucket.core.search.html.code(
if (query.nonEmpty) searchFiles(repository.owner, repository.name, query) else Nil,
query,
page,
repository
)
}
} }
}) })
get("/search") { get("/search"){
val query = params.getOrElse("query", "").trim.toLowerCase val query = params.getOrElse("query", "").trim.toLowerCase
val visibleRepositories = val visibleRepositories = getVisibleRepositories(context.loginAccount, repositoryUserName = None, withoutPhysicalInfo = true)
getVisibleRepositories( val repositories = visibleRepositories.filter { repository =>
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
)
val repositories = {
context.settings.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 repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0
} }
context.loginAccount.map { account =>
gitbucket.core.search.html.repositories(query, repositories, visibleRepositories) gitbucket.core.search.html.repositories(query, repositories, Nil, getUserRepositories(account.userName, withoutPhysicalInfo = true))
}.getOrElse {
gitbucket.core.search.html.repositories(query, repositories, visibleRepositories, Nil)
}
} }
} }

View File

@@ -11,25 +11,23 @@ import gitbucket.core.view.Markdown
import org.scalatra.forms._ import org.scalatra.forms._
import org.scalatra.{BadRequest, Ok} import org.scalatra.{BadRequest, Ok}
class IssuesController
extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with IssuesService
with RepositoryService with RepositoryService
with AccountService with AccountService
with LabelsService with LabelsService
with MilestonesService with MilestonesService
with ActivityService with ActivityService
with HandleCommentService with HandleCommentService
with IssueCreationService with IssueCreationService
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with WritableUsersAuthenticator with WritableUsersAuthenticator
with MergeService with PullRequestService
with PullRequestService with WebHookIssueCommentService
with WebHookIssueCommentService with CommitsService
with WebHookPullRequestReviewCommentService with PrioritiesService
with CommitsService
with PrioritiesService
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService self: IssuesService
@@ -47,46 +45,40 @@ trait IssuesControllerBase extends ControllerBase {
with WebHookIssueCommentService with WebHookIssueCommentService
with PrioritiesService => with PrioritiesService =>
case class IssueCreateForm( case class IssueCreateForm(title: String, content: Option[String],
title: String, assignedUserName: Option[String], milestoneId: Option[Int], priorityId: Option[Int], labelNames: Option[String])
content: Option[String],
assignedUserName: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Option[String]
)
case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String]) case class IssueStateForm(issueId: Int, content: Option[String])
val issueCreateForm = mapping( val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())), "content" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())), "assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())), "milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())), "priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
val issueTitleEditForm = mapping( val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required))) "title" -> trim(label("Title", text(required)))
)(x => x) )(x => x)
val issueEditForm = mapping( val issueEditForm = mapping(
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(x => x) )(x => x)
val commentForm = mapping( val commentForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(label("Comment", text(required))) "content" -> trim(label("Comment", text(required)))
)(CommentForm.apply) )(CommentForm.apply)
val issueStateForm = mapping( val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { repository => get("/:owner/:repository/issues")(referrersOnly { repository =>
val q = request.getParameter("q") val q = request.getParameter("q")
if (Option(q).exists(_.contains("is:pr"))) { if(Option(q).exists(_.contains("is:pr"))){
redirect(s"/${repository.owner}/${repository.name}/pulls?q=${StringUtil.urlEncode(q)}") redirect(s"/${repository.owner}/${repository.name}/pulls?q=${StringUtil.urlEncode(q)}")
} else { } else {
searchIssues(repository) searchIssues(repository)
@@ -94,50 +86,45 @@ trait IssuesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id")) { defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
case (owner, name, issueId) => getIssue(owner, name, issueId) map { issue =>
getIssue(owner, name, issueId) map { if(issue.isPullRequest){
issue => redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
if (issue.isPullRequest) { } else {
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") html.issue(
} else { issue,
html.issue( getComments(owner, name, issueId.toInt),
issue, getIssueLabels(owner, name, issueId.toInt),
getComments(owner, name, issueId.toInt), getAssignableUserNames(owner, name),
getIssueLabels(owner, name, issueId.toInt), getMilestonesWithIssueCount(owner, name),
getAssignableUserNames(owner, name), getPriorities(owner, name),
getMilestonesWithIssueCount(owner, name), getLabels(owner, name),
getPriorities(owner, name), isIssueEditable(repository),
getLabels(owner, name), isIssueManageable(repository),
isIssueEditable(repository), repository)
isIssueManageable(repository), }
repository } getOrElse NotFound()
)
}
} getOrElse NotFound()
} }
}) })
get("/:owner/:repository/issues/new")(readableUsersOnly { repository => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator? if(isIssueEditable(repository)){ // TODO Should this check is provided by authenticator?
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, name) =>
case (owner, name) => html.create(
html.create( getAssignableUserNames(owner, name),
getAssignableUserNames(owner, name), getMilestones(owner, name),
getMilestones(owner, name), getPriorities(owner, name),
getPriorities(owner, name), getDefaultPriority(owner, name),
getDefaultPriority(owner, name), getLabels(owner, name),
getLabels(owner, name), isIssueManageable(repository),
isIssueManageable(repository), getContentTemplate(repository, "ISSUE_TEMPLATE"),
getContentTemplate(repository, "ISSUE_TEMPLATE"), repository)
repository
)
} }
} else Unauthorized() } else Unauthorized()
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator? if(isIssueEditable(repository)){ // TODO Should this check is provided by authenticator?
val issue = createIssue( val issue = createIssue(
repository, repository,
form.title, form.title,
@@ -145,159 +132,134 @@ trait IssuesControllerBase extends ControllerBase {
form.assignedUserName, form.assignedUserName,
form.milestoneId, form.milestoneId,
form.priorityId, form.priorityId,
form.labelNames.toSeq.flatMap(_.split(",")), form.labelNames.toArray.flatMap(_.split(",")),
context.loginAccount.get context.loginAccount.get)
)
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}") redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
} else Unauthorized() } else Unauthorized()
}) })
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, name) =>
case (owner, name) => getIssue(owner, name, params("id")).map { issue =>
getIssue(owner, name, params("id")).map { if(isEditableContent(owner, name, issue.openedUserName)){
issue => // update issue
if (isEditableContent(owner, name, issue.openedUserName)) { updateIssue(owner, name, issue.issueId, title, issue.content)
if (issue.title != title) { // extract references and create refer comment
// update issue createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get) } else Unauthorized()
createComment( } getOrElse NotFound()
owner,
name,
context.loginAccount.get.userName,
issue.issueId,
issue.title + "\r\n" + title,
"change_title"
)
}
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized()
} getOrElse NotFound()
} }
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, name) =>
case (owner, name) => getIssue(owner, name, params("id")).map { issue =>
getIssue(owner, name, params("id")).map { issue => if(isEditableContent(owner, name, issue.openedUserName)){
if (isEditableContent(owner, name, issue.openedUserName)) { // update issue
// update issue updateIssue(owner, name, issue.issueId, issue.title, content)
updateIssue(owner, name, issue.issueId, issue.title, content) // extract references and create refer comment
// extract references and create refer comment createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized() } else Unauthorized()
} getOrElse NotFound() } getOrElse NotFound()
} }
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt = val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName))
params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName)) handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
handleComment(issue, Some(form.content), repository, actionOpt) map { redirect(s"/${repository.owner}/${repository.name}/${
case (issue, id) => if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
)
} }
} getOrElse NotFound() } getOrElse NotFound()
}) })
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt = val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName))
params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName)) handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
handleComment(issue, form.content, repository, actionOpt) map { redirect(s"/${repository.owner}/${repository.name}/${
case (issue, id) => if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
redirect(
s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
)
} }
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, name) =>
case (owner, name) => getComment(owner, name, params("id")).map { comment =>
getComment(owner, name, params("id")).map { comment => if(isEditableContent(owner, name, comment.commentedUserName)){
if (isEditableContent(owner, name, comment.commentedUserName)) { updateComment(comment.issueId, comment.commentId, form.content)
updateComment(comment.issueId, comment.commentId, form.content) redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") } else Unauthorized()
} else Unauthorized() } getOrElse NotFound()
} getOrElse NotFound()
} }
}) })
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, name) =>
case (owner, name) => getComment(owner, name, params("id")).map { comment =>
getComment(owner, name, params("id")).map { comment => if(isEditableContent(owner, name, comment.commentedUserName)){
if (isEditableContent(owner, name, comment.commentedUserName)) { Ok(deleteComment(comment.issueId, comment.commentId))
Ok(deleteComment(comment.issueId, comment.commentId)) } else Unauthorized()
} else Unauthorized() } getOrElse NotFound()
} getOrElse NotFound()
} }
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { getIssue(repository.owner, repository.name, params("id")) map { x =>
x => if(isEditableContent(x.userName, x.repositoryName, x.openedUserName)){
if (isEditableContent(x.userName, x.repositoryName, x.openedUserName)) { params.get("dataType") collect {
params.get("dataType") collect { case t if t == "html" => html.editissue(x.content, x.issueId, repository)
case t if t == "html" => html.editissue(x.content, x.issueId, repository) } getOrElse {
} getOrElse { contentType = formats("json")
contentType = formats("json") org.json4s.jackson.Serialization.write(
org.json4s.jackson.Serialization.write( Map(
Map( "title" -> x.title,
"title" -> x.title, "content" -> Markdown.toHtml(
"content" -> Markdown.toHtml( markdown = x.content getOrElse "No description given.",
markdown = x.content getOrElse "No description given.", repository = repository,
repository = repository, enableWikiLink = false,
branch = repository.repository.defaultBranch, enableRefsLink = true,
enableWikiLink = false, enableAnchor = true,
enableRefsLink = true, enableLineBreaks = true,
enableAnchor = true, enableTaskList = true,
enableLineBreaks = true, hasWritePermission = true
enableTaskList = true,
hasWritePermission = true
)
) )
) )
} )
} else Unauthorized() }
} else Unauthorized()
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
getComment(repository.owner, repository.name, params("id")) map { getComment(repository.owner, repository.name, params("id")) map { x =>
x => if(isEditableContent(x.userName, x.repositoryName, x.commentedUserName)){
if (isEditableContent(x.userName, x.repositoryName, x.commentedUserName)) { params.get("dataType") collect {
params.get("dataType") collect { case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
case t if t == "html" => html.editcomment(x.content, x.commentId, repository) } getOrElse {
} getOrElse { contentType = formats("json")
contentType = formats("json") org.json4s.jackson.Serialization.write(
org.json4s.jackson.Serialization.write( Map(
Map( "content" -> view.Markdown.toHtml(
"content" -> view.Markdown.toHtml( markdown = x.content,
markdown = x.content, repository = repository,
repository = repository, enableWikiLink = false,
branch = repository.repository.defaultBranch, enableRefsLink = true,
enableWikiLink = false, enableAnchor = true,
enableRefsLink = true, enableLineBreaks = true,
enableAnchor = true, enableTaskList = true,
enableLineBreaks = true, hasWritePermission = true
enableTaskList = true,
hasWritePermission = true
)
) )
) )
} )
} else Unauthorized() }
} else Unauthorized()
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -308,109 +270,97 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository =>
defining(params("id").toInt) { issueId => defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true) registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
} }
}) })
ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository =>
defining(params("id").toInt) { issueId => defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true) deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
} }
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
updateAssignedUserName( updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
repository.owner,
repository.name,
params("id").toInt,
assignedUserName("assignedUserName"),
true
)
Ok("updated") Ok("updated")
}) })
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"), true) updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId) .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
.map { gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
case (_, openCount, closeCount) => } getOrElse NotFound()
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound()
} getOrElse Ok() } getOrElse Ok()
}) })
ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository =>
val priority = priorityId("priorityId") updatePriorityId(repository.owner, repository.name, params("id").toInt, priorityId("priorityId"))
updatePriorityId(repository.owner, repository.name, params("id").toInt, priority, true)
Ok("updated") Ok("updated")
}) })
post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
defining(params.get("value")) { defining(params.get("value")){ action =>
action => action match {
action match { case Some("open") => executeBatch(repository) { issueId =>
case Some("open") => getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
executeBatch(repository) { issueId => handleComment(issue, None, repository, Some("reopen"))
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => }
handleComment(issue, None, repository, Some("reopen"))
}
}
case Some("close") =>
executeBatch(repository) { issueId =>
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
handleComment(issue, None, repository, Some("close"))
}
}
case _ => BadRequest()
} }
case Some("close") => executeBatch(repository) { issueId =>
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
handleComment(issue, None, repository, Some("close"))
}
}
case _ => BadRequest()
}
} }
}) })
post("/:owner/:repository/issues/batchedit/label")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/label")(writableUsersOnly { repository =>
params("value").toIntOpt.map { labelId => params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { issueId => executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId, true) registerIssueLabel(repository.owner, repository.name, issueId, labelId)
} }
} }
} getOrElse NotFound() } getOrElse NotFound()
}) })
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
defining(assignedUserName("value")) { value => defining(assignedUserName("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value, true) updateAssignedUserName(repository.owner, repository.name, _, value)
} }
} }
}) })
post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository =>
defining(milestoneId("value")) { value => defining(milestoneId("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value, true) updateMilestoneId(repository.owner, repository.name, _, value)
} }
} }
}) })
post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository =>
defining(priorityId("value")) { value => defining(priorityId("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updatePriorityId(repository.owner, repository.name, _, value, true) updatePriorityId(repository.owner, repository.name, _, value)
} }
} }
}) })
get("/:owner/:repository/_attached/:file")(referrersOnly { repository => get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
(Directory.getAttachedDir(repository.owner, repository.name) match { (Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if (dir.exists && dir.isDirectory) => case dir if(dir.exists && dir.isDirectory) =>
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""") response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
RawData(FileUtil.getSafeMimeType(file.getName), file) RawData(FileUtil.getMimeType(file.getName), file)
} }
case _ => None case _ => None
}) getOrElse NotFound() }) getOrElse NotFound()
@@ -421,7 +371,7 @@ trait IssuesControllerBase extends ControllerBase {
val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map (_.toInt) foreach execute params("checked").split(',') map(_.toInt) foreach execute
params("from") match { params("from") match {
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls") case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
@@ -429,14 +379,13 @@ trait IssuesControllerBase extends ControllerBase {
} }
private def searchIssues(repository: RepositoryService.RepositoryInfo) = { private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, repoName) =>
case (owner, repoName) => val page = IssueSearchCondition.page(request)
val page = IssueSearchCondition.page(request)
// retrieve search condition // retrieve search condition
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
html.list( html.list(
"issues", "issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
@@ -444,22 +393,19 @@ trait IssuesControllerBase extends ControllerBase {
getMilestones(owner, repoName), getMilestones(owner, repoName),
getPriorities(owner, repoName), getPriorities(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), false, owner -> repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition, condition,
repository, repository,
isIssueEditable(repository), isIssueEditable(repository),
isIssueManageable(repository) isIssueManageable(repository))
)
} }
} }
/** /**
* Tests whether an issue or a comment is editable by a logged-in user. * Tests whether an issue or a comment is editable by a logged-in user.
*/ */
private def isEditableContent(owner: String, repository: String, author: String)( private def isEditableContent(owner: String, repository: String, author: String)(implicit context: Context): Boolean = {
implicit context: Context
): Boolean = {
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
} }
} }

View File

@@ -1,14 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.issues.labels.html import gitbucket.core.issues.labels.html
import gitbucket.core.service.{ import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
RepositoryService,
AccountService,
IssuesService,
LabelsService,
MilestonesService,
PrioritiesService
}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
@@ -16,38 +9,28 @@ import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok import org.scalatra.Ok
class LabelsController class LabelsController extends LabelsControllerBase
extends LabelsControllerBase with LabelsService with IssuesService with RepositoryService with AccountService
with IssuesService with ReferrerAuthenticator with WritableUsersAuthenticator
with RepositoryService
with AccountService
with LabelsService
with PrioritiesService
with MilestonesService
with ReferrerAuthenticator
with WritableUsersAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService self: LabelsService with IssuesService with RepositoryService
with IssuesService with ReferrerAuthenticator with WritableUsersAuthenticator =>
with RepositoryService
with ReferrerAuthenticator
with WritableUsersAuthenticator =>
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
val labelForm = mapping( val labelForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))), "labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color))) "labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
get("/:owner/:repository/issues/labels")(referrersOnly { repository => get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
html.list( html.list(
getLabels(repository.owner, repository.name), getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
)
}) })
ajaxGet("/:owner/:repository/issues/labels/new")(writableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/labels/new")(writableUsersOnly { repository =>
@@ -61,8 +44,7 @@ trait LabelsControllerBase extends ControllerBase {
// TODO futility // TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
)
}) })
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(writableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(writableUsersOnly { repository =>
@@ -78,8 +60,7 @@ trait LabelsControllerBase extends ControllerBase {
// TODO futility // TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
)
}) })
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(writableUsersOnly { repository =>
@@ -90,34 +71,26 @@ trait LabelsControllerBase extends ControllerBase {
/** /**
* Constraint for the identifier such as user name, repository name or page name. * Constraint for the identifier such as user name, repository name or page name.
*/ */
private def labelName: Constraint = new Constraint() { private def labelName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (value.contains(',')) { if(value.contains(',')){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if (value.startsWith("_") || value.startsWith("-")) { } else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.") Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }
} }
private def uniqueLabelName: Constraint = new Constraint() { private def uniqueLabelName: Constraint = new Constraint(){
override def validate( override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
name: String, val owner = params.value("owner")
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] = {
val owner = params.value("owner")
val repository = params.value("repository") val repository = params.value("repository")
params params.optionValue("labelId").map { labelId =>
.optionValue("labelId") getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.")
.map { labelId => }.getOrElse {
getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.") getLabel(owner, repository, value).map(_ => "Name has already been taken.")
} }
.getOrElse {
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
}
} }
} }

View File

@@ -1,30 +1,25 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.issues.milestones.html import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{AccountService, MilestonesService, RepositoryService} import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.Implicits._
import org.scalatra.forms._ import org.scalatra.forms._
import org.scalatra.i18n.Messages
class MilestonesController class MilestonesController extends MilestonesControllerBase
extends MilestonesControllerBase with MilestonesService with RepositoryService with AccountService
with MilestonesService with ReferrerAuthenticator with WritableUsersAuthenticator
with RepositoryService
with AccountService
with ReferrerAuthenticator
with WritableUsersAuthenticator
trait MilestonesControllerBase extends ControllerBase { trait MilestonesControllerBase extends ControllerBase {
self: MilestonesService with RepositoryService with ReferrerAuthenticator with WritableUsersAuthenticator => self: MilestonesService with RepositoryService
with ReferrerAuthenticator with WritableUsersAuthenticator =>
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])
val milestoneForm = mapping( val milestoneForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100), uniqueMilestone))), "title" -> trim(label("Title", text(required, maxlength(100)))),
"description" -> trim(label("Description", optional(text()))), "description" -> trim(label("Description", optional(text()))),
"dueDate" -> trim(label("Due Date", optional(date()))) "dueDate" -> trim(label("Due Date", optional(date())))
)(MilestoneForm.apply) )(MilestoneForm.apply)
get("/:owner/:repository/issues/milestones")(referrersOnly { repository => get("/:owner/:repository/issues/milestones")(referrersOnly { repository =>
@@ -32,8 +27,7 @@ trait MilestonesControllerBase extends ControllerBase {
params.getOrElse("state", "open"), params.getOrElse("state", "open"),
getMilestonesWithIssueCount(repository.owner, repository.name), getMilestonesWithIssueCount(repository.owner, repository.name),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
)
}) })
get("/:owner/:repository/issues/milestones/new")(writableUsersOnly { get("/:owner/:repository/issues/milestones/new")(writableUsersOnly {
@@ -46,23 +40,22 @@ trait MilestonesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(writableUsersOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/edit")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.map { milestoneId => params("milestoneId").toIntOpt.map{ milestoneId =>
html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository) html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
} getOrElse NotFound() } getOrElse NotFound()
}) })
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(writableUsersOnly { post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(writableUsersOnly { (form, repository) =>
(form, repository) => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
params("milestoneId").toIntOpt.flatMap { milestoneId => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") }
} } getOrElse NotFound()
} getOrElse NotFound()
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/close")(writableUsersOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/close")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.flatMap { milestoneId => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
closeMilestone(milestone) closeMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
@@ -71,7 +64,7 @@ trait MilestonesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/open")(writableUsersOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/open")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.flatMap { milestoneId => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
openMilestone(milestone) openMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
@@ -80,7 +73,7 @@ trait MilestonesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(writableUsersOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/delete")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.flatMap { milestoneId => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId) deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
@@ -88,29 +81,4 @@ trait MilestonesControllerBase extends ControllerBase {
} getOrElse NotFound() } getOrElse NotFound()
}) })
private def uniqueMilestone: Constraint = new Constraint() {
override def validate(
name: String,
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] = {
for {
owner <- params.optionValue("owner")
repository <- params.optionValue("repository")
_ <- params.optionValue("milestoneId") match {
// existing milestone
case Some(id) =>
getMilestones(owner, repository)
.find(m => m.title.equalsIgnoreCase(value) && m.milestoneId.toString != id)
// new milestone
case None =>
getMilestones(owner, repository)
.find(m => m.title.equalsIgnoreCase(value))
}
} yield {
"Milestone already exists."
}
}
}
} }

View File

@@ -1,6 +1,5 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.plugin.PluginRegistry
import org.scalatra.MovedPermanently import org.scalatra.MovedPermanently
class PreProcessController extends PreProcessControllerBase class PreProcessController extends PreProcessControllerBase
@@ -8,21 +7,20 @@ class PreProcessController extends PreProcessControllerBase
trait PreProcessControllerBase extends ControllerBase { trait PreProcessControllerBase extends ControllerBase {
/** /**
* Provides GitHub compatible URLs (e.g. http://localhost:8080/owner/repo.git) for Git client. * Provides GitHub compatible URLs for Git client.
*
* <ul>
* <li>git clone http://localhost:8080/owner/repo</li>
* <li>git clone http://localhost:8080/owner/repo.git</li>
* </ul>
*
* @see https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
*/ */
get("/*/*/info/refs") { get("/*/*/info/refs") {
val query = Option(request.getQueryString).map("?" + _).getOrElse("") val query = Option(request.getQueryString).map("?" + _).getOrElse("")
halt(MovedPermanently(baseUrl + "/git" + request.getRequestURI + query)) halt(MovedPermanently(baseUrl + "/git" + request.getRequestURI + query))
} }
/**
* Provides GitHub compatible URLs for GitLFS client.
*/
post("/*/*/info/lfs/objects/batch") {
val dispatcher = request.getRequestDispatcher("/git" + request.getRequestURI)
dispatcher.forward(request, response)
}
/** /**
* Filter requests from anonymous users. * Filter requests from anonymous users.
* *
@@ -30,15 +28,13 @@ trait PreProcessControllerBase extends ControllerBase {
* But if it's not allowed, demands authentication except some paths. * But if it's not allowed, demands authentication except some paths.
*/ */
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) { get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") && if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") && !context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs")) {
!PluginRegistry().getAnonymousAccessiblePaths().exists { path =>
context.currentPath.startsWith(path)
}) {
Unauthorized() Unauthorized()
} else { } else {
pass() pass()
} }
} }
} }

View File

@@ -1,14 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.issues.priorities.html import gitbucket.core.issues.priorities.html
import gitbucket.core.service.{ import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, PrioritiesService}
RepositoryService,
AccountService,
IssuesService,
LabelsService,
MilestonesService,
PrioritiesService
}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
@@ -16,39 +9,29 @@ import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok import org.scalatra.Ok
class PrioritiesController class PrioritiesController extends PrioritiesControllerBase
extends PrioritiesControllerBase with PrioritiesService with IssuesService with RepositoryService with AccountService
with IssuesService with ReferrerAuthenticator with WritableUsersAuthenticator
with RepositoryService
with AccountService
with LabelsService
with PrioritiesService
with MilestonesService
with ReferrerAuthenticator
with WritableUsersAuthenticator
trait PrioritiesControllerBase extends ControllerBase { trait PrioritiesControllerBase extends ControllerBase {
self: PrioritiesService self: PrioritiesService with IssuesService with RepositoryService
with IssuesService with ReferrerAuthenticator with WritableUsersAuthenticator =>
with RepositoryService
with ReferrerAuthenticator
with WritableUsersAuthenticator =>
case class PriorityForm(priorityName: String, description: Option[String], color: String) case class PriorityForm(priorityName: String, description: Option[String], color: String)
val priorityForm = mapping( val priorityForm = mapping(
"priorityName" -> trim(label("Priority name", text(required, priorityName, uniquePriorityName, maxlength(100)))), "priorityName" -> trim(label("Priority name", text(required, priorityName, uniquePriorityName, maxlength(100)))),
"description" -> trim(label("Description", optional(text(maxlength(255))))), "description" -> trim(label("Description", optional(text(maxlength(255))))),
"priorityColor" -> trim(label("Color", text(required, color))) "priorityColor" -> trim(label("Color", text(required, color)))
)(PriorityForm.apply) )(PriorityForm.apply)
get("/:owner/:repository/issues/priorities")(referrersOnly { repository => get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
html.list( html.list(
getPriorities(repository.owner, repository.name), getPriorities(repository.owner, repository.name),
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
)
}) })
ajaxGet("/:owner/:repository/issues/priorities/new")(writableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/priorities/new")(writableUsersOnly { repository =>
@@ -56,14 +39,12 @@ trait PrioritiesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/priorities/new", priorityForm)(writableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/priorities/new", priorityForm)(writableUsersOnly { (form, repository) =>
val priorityId = val priorityId = createPriority(repository.owner, repository.name, form.priorityName, form.description, form.color.substring(1))
createPriority(repository.owner, repository.name, form.priorityName, form.description, form.color.substring(1))
html.priority( html.priority(
getPriority(repository.owner, repository.name, priorityId).get, getPriority(repository.owner, repository.name, priorityId).get,
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
)
}) })
ajaxGet("/:owner/:repository/issues/priorities/:priorityId/edit")(writableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/priorities/:priorityId/edit")(writableUsersOnly { repository =>
@@ -72,34 +53,21 @@ trait PrioritiesControllerBase extends ControllerBase {
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issues/priorities/:priorityId/edit", priorityForm)(writableUsersOnly { ajaxPost("/:owner/:repository/issues/priorities/:priorityId/edit", priorityForm)(writableUsersOnly { (form, repository) =>
(form, repository) => updatePriority(repository.owner, repository.name, params("priorityId").toInt, form.priorityName, form.description, form.color.substring(1))
updatePriority( html.priority(
repository.owner, getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
repository.name, countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
params("priorityId").toInt, repository,
form.priorityName, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
form.description,
form.color.substring(1)
)
html.priority(
getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
}) })
ajaxPost("/:owner/:repository/issues/priorities/reorder")(writableUsersOnly { (repository) => ajaxPost("/:owner/:repository/issues/priorities/reorder")(writableUsersOnly { (repository) =>
reorderPriorities( reorderPriorities(repository.owner, repository.name, params("order")
repository.owner, .split(",")
repository.name, .map(id => id.toInt)
params("order") .zipWithIndex
.split(",") .toMap)
.map(id => id.toInt)
.zipWithIndex
.toMap
)
Ok() Ok()
}) })
@@ -119,36 +87,26 @@ trait PrioritiesControllerBase extends ControllerBase {
/** /**
* Constraint for the identifier such as user name, repository name or page name. * Constraint for the identifier such as user name, repository name or page name.
*/ */
private def priorityName: Constraint = new Constraint() { private def priorityName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (value.contains(',')) { if(value.contains(',')){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if (value.startsWith("_") || value.startsWith("-")) { } else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.") Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }
} }
private def uniquePriorityName: Constraint = new Constraint() { private def uniquePriorityName: Constraint = new Constraint(){
override def validate( override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
name: String, val owner = params.value("owner")
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] = {
val owner = params.value("owner")
val repository = params.value("repository") val repository = params.value("repository")
params params.optionValue("priorityId").map { priorityId =>
.optionValue("priorityId") getPriority(owner, repository, value).filter(_.priorityId != priorityId.toInt).map(_ => "Name has already been taken.")
.map { priorityId => }.getOrElse {
getPriority(owner, repository, value) getPriority(owner, repository, value).map(_ => "Name has already been taken.")
.filter(_.priorityId != priorityId.toInt) }
.map(_ => "Name has already been taken.")
}
.getOrElse {
getPriority(owner, repository, value).map(_ => "Name has already been taken.")
}
} }
} }
} }

View File

@@ -1,5 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.model.WebHook
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.pulls.html import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService import gitbucket.core.service.MergeService
@@ -13,69 +15,42 @@ import gitbucket.core.util.Implicits._
import gitbucket.core.util._ import gitbucket.core.util._
import org.scalatra.forms._ import org.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.BadRequest import org.eclipse.jgit.lib.PersonIdent
import scala.util.Using import scala.collection.JavaConverters._
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with CommitsService with ActivityService with WebHookPullRequestService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
with CommitStatusService with MergeService with ProtectedBranchService with PrioritiesService
class PullRequestsController
extends PullRequestsControllerBase
with RepositoryService
with AccountService
with IssuesService
with PullRequestService
with MilestonesService
with LabelsService
with CommitsService
with ActivityService
with WebHookPullRequestService
with WebHookPullRequestReviewCommentService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with CommitStatusService
with MergeService
with ProtectedBranchService
with PrioritiesService
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with AccountService with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService
with IssuesService with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
with MilestonesService with CommitStatusService with MergeService with ProtectedBranchService with PrioritiesService =>
with LabelsService
with CommitsService
with ActivityService
with PullRequestService
with WebHookPullRequestService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with CommitStatusService
with MergeService
with ProtectedBranchService
with PrioritiesService =>
val pullRequestForm = mapping( val pullRequestForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))), "title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))), "content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))), "targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))), "targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))), "requestUserName" -> trim(text(required, maxlength(100))),
"requestRepositoryName" -> trim(text(required, maxlength(100))), "requestRepositoryName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))), "requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))), "commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40))), "commitIdTo" -> trim(text(required, maxlength(40))),
"isDraft" -> trim(boolean(required)), "assignedUserName" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())), "milestoneId" -> trim(optional(number())),
"milestoneId" -> trim(optional(number())), "priorityId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())), "labelNames" -> trim(optional(text()))
"labelNames" -> trim(optional(text()))
)(PullRequestForm.apply) )(PullRequestForm.apply)
val mergeForm = mapping( val mergeForm = mapping(
"message" -> trim(label("Message", text(required))), "message" -> trim(label("Message", text(required)))
"strategy" -> trim(label("Strategy", text(required))),
"isDraft" -> trim(boolean(required))
)(MergeForm.apply) )(MergeForm.apply)
case class PullRequestForm( case class PullRequestForm(
@@ -88,18 +63,17 @@ trait PullRequestsControllerBase extends ControllerBase {
requestBranch: String, requestBranch: String,
commitIdFrom: String, commitIdFrom: String,
commitIdTo: String, commitIdTo: String,
isDraft: Boolean,
assignedUserName: Option[String], assignedUserName: Option[String],
milestoneId: Option[Int], milestoneId: Option[Int],
priorityId: Option[Int], priorityId: Option[Int],
labelNames: Option[String] labelNames: Option[String]
) )
case class MergeForm(message: String, strategy: String, isDraft: Boolean) case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository => get("/:owner/:repository/pulls")(referrersOnly { repository =>
val q = request.getParameter("q") val q = request.getParameter("q")
if (Option(q).exists(_.contains("is:issue"))) { if(Option(q).exists(_.contains("is:issue"))){
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q)) redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
} else { } else {
searchPullRequests(None, repository) searchPullRequests(None, repository)
@@ -107,149 +81,66 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { params("id").toIntOpt.flatMap{ issueId =>
issueId => val owner = repository.owner
val owner = repository.owner val name = repository.name
val name = repository.name getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
getPullRequest(owner, name, issueId) map { using(Git.open(getRepositoryDir(owner, name))){ git =>
case (issue, pullreq) => val (commits, diffs) =
val (commits, diffs) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) html.pullreq(
issue, pullreq,
html.conversation( (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
issue, .sortWith((a, b) => a.registeredDate before b.registeredDate),
pullreq, getIssueLabels(owner, name, issueId),
commits.flatten, getAssignableUserNames(owner, name),
getPullRequestComments(owner, name, issue.issueId, commits.flatten), getMilestonesWithIssueCount(owner, name),
diffs.size, getPriorities(owner, name),
getIssueLabels(owner, name, issueId), getLabels(owner, name),
getAssignableUserNames(owner, name), commits,
getMilestonesWithIssueCount(owner, name), diffs,
getPriorities(owner, name), isEditable(repository),
getLabels(owner, name), isManageable(repository),
isEditable(repository), hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
isManageable(repository), repository,
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount), getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
repository, flash.toMap.map(f => f._1 -> f._2.toString))
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
flash.iterator.map(f => f._1 -> f._2.toString).toMap
)
// html.pullreq(
// issue,
// pullreq,
// comments,
// getIssueLabels(owner, name, issueId),
// getAssignableUserNames(owner, name),
// getMilestonesWithIssueCount(owner, name),
// getPriorities(owner, name),
// getLabels(owner, name),
// commits,
// diffs,
// isEditable(repository),
// isManageable(repository),
// hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
// repository,
// getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
// flash.toMap.map(f => f._1 -> f._2.toString)
// )
}
} getOrElse NotFound()
})
get("/:owner/:repository/pull/:id/commits")(referrersOnly { repository =>
params("id").toIntOpt.flatMap {
issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
html.commits(
issue,
pullreq,
commits,
getPullRequestComments(owner, 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 =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map {
case (issue, pullreq) =>
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
html.files(
issue,
pullreq,
diffs,
commits.flatten,
getPullRequestComments(owner, name, issue.issueId, commits.flatten),
isManageable(repository),
repository
)
} }
}
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository => ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
params("id").toIntOpt.flatMap { params("id").toIntOpt.flatMap{ issueId =>
issueId => val owner = repository.owner
val owner = repository.owner val name = repository.name
val name = repository.name getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
getPullRequest(owner, name, issueId) map { val hasConflict = LockUtil.lock(s"${owner}/${name}"){
case (issue, pullreq) => checkConflict(owner, name, pullreq.branch, issueId)
val conflictMessage = LockUtil.lock(s"${owner}/${name}") {
checkConflict(owner, name, pullreq.branch, issueId)
}
val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
conflictMessage = conflictMessage,
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
branchProtection = branchProtection,
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
needStatusCheck = context.loginAccount
.map { u =>
branchProtection.needStatusCheck(u.userName)
}
.getOrElse(true),
hasUpdatePermission = hasDeveloperRole(
pullreq.requestUserName,
pullreq.requestRepositoryName,
context.loginAccount
) &&
context.loginAccount
.map { u =>
!getProtectedBranchInfo(
pullreq.requestUserName,
pullreq.requestRepositoryName,
pullreq.requestBranch
).needStatusCheck(u.userName)
}
.getOrElse(false),
hasMergePermission = hasMergePermission,
commitIdTo = pullreq.commitIdTo
)
html.mergeguide(
mergeStatus,
issue,
pullreq,
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get
)
} }
val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
hasConflict = hasConflict,
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
branchProtection = branchProtection,
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
needStatusCheck = context.loginAccount.map{ u =>
branchProtection.needStatusCheck(u.userName)
}.getOrElse(true),
hasUpdatePermission = hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
context.loginAccount.map{ u =>
!getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName)
}.getOrElse(false),
hasMergePermission = hasMergePermission,
commitIdTo = pullreq.commitIdTo)
html.mergeguide(
mergeStatus,
issue,
pullreq,
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
}
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -259,33 +150,25 @@ trait PullRequestsControllerBase extends ControllerBase {
loginAccount <- context.loginAccount loginAccount <- context.loginAccount
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId) (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName owner = pullreq.requestUserName
name = pullreq.requestRepositoryName name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount) if hasDeveloperRole(owner, name, context.loginAccount)
} yield { } yield {
val repository = getRepository(owner, name).get val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch) val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if (branchProtection.enabled) { if(branchProtection.enabled){
flash.update("error", s"branch ${pullreq.requestBranch} is protected.") flash += "error" -> s"branch ${pullreq.requestBranch} is protected."
} else { } else {
if (repository.repository.defaultBranch != pullreq.requestBranch) { if(repository.repository.defaultBranch != pullreq.requestBranch){
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call() git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, pullreq.requestBranch) recordDeleteBranchActivity(repository.owner, repository.name, userName, pullreq.requestBranch)
} }
createComment( createComment(baseRepository.owner, baseRepository.name, userName, issueId, pullreq.requestBranch, "delete_branch")
baseRepository.owner,
baseRepository.name,
userName,
issueId,
pullreq.requestBranch,
"delete_branch"
)
} else { } else {
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""") flash += "error" -> s"""Can't delete the default branch "${pullreq.requestBranch}"."""
} }
} }
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}") redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
}) getOrElse NotFound() }) getOrElse NotFound()
}) })
@@ -295,44 +178,62 @@ trait PullRequestsControllerBase extends ControllerBase {
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount loginAccount <- context.loginAccount
(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 owner = pullreq.requestUserName
name = pullreq.requestRepositoryName name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount) if hasDeveloperRole(owner, name, context.loginAccount)
} yield { } yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch) val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if (branchProtection.needStatusCheck(loginAccount.userName)) { if(branchProtection.needStatusCheck(loginAccount.userName)){
flash.update("error", s"branch ${pullreq.requestBranch} is protected need status check.") flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
} else { } else {
LockUtil.lock(s"${owner}/${name}") { LockUtil.lock(s"${owner}/${name}"){
val alias = val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
if (pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName) { pullreq.branch
pullreq.branch } else {
} else { s"${pullreq.userName}:${pullreq.branch}"
s"${pullreq.userName}:${pullreq.branch}" }
} val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
val existIds = Using pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
.resource(Git.open(Directory.getRepositoryDir(owner, name))) { git => s"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
JGitUtil.getAllCommitIds(git)
}
.toSet
pullRemote(
repository,
pullreq.requestBranch,
remoteRepository,
pullreq.branch,
loginAccount,
s"Merge branch '${alias}' into ${pullreq.requestBranch}",
Some(pullreq),
context.settings
) match {
case None => // conflict case None => // conflict
flash.update("error", s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}.") flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
case Some(oldId) => case Some(oldId) =>
// update pull request // update pull request
updatePullRequests(owner, name, pullreq.requestBranch, loginAccount, "synchronize", context.settings) updatePullRequests(owner, name, pullreq.requestBranch)
flash.update("info", s"Merge branch '${alias}' into ${pullreq.requestBranch}")
using(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
// after update branch
val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}")
val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList
commits.foreach { commit =>
if(!existIds.contains(commit.id)){
createIssueComment(owner, name, commit)
}
}
// record activity
recordPushActivity(owner, name, loginAccount.userName, pullreq.branch, commits)
// close issue by commit message
if(pullreq.requestBranch == repository.repository.defaultBranch){
commits.map { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
}
// call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, pullreq.requestBranch, baseUrl, loginAccount)
callWebHookOf(owner, name, WebHook.Push) {
for {
ownerAccount <- getAccountByUserName(owner)
} yield {
WebHookService.WebHookPushPayload(git, loginAccount, pullreq.requestBranch, repository, commits, ownerAccount, oldId = oldId, newId = newCommitId)
}
}
}
flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}"
} }
} }
} }
@@ -341,66 +242,78 @@ trait PullRequestsControllerBase extends ControllerBase {
}) getOrElse NotFound() }) getOrElse NotFound()
}) })
post("/:owner/:repository/pull/:id/update_draft")(readableUsersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
(_, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount)
} yield {
updateDraftToPullRequest(baseRepository.owner, baseRepository.name, issueId)
}) getOrElse NotFound()
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(writableUsersOnly { (form, repository) => post("/:owner/:repository/pull/:id/merge", mergeForm)(writableUsersOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
val commentId = createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
mergePullRequest( // record activity
repository, recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
issueId,
context.loginAccount.get, // merge git repository
form.message, mergePullRequest(git, pullreq.branch, issueId,
form.strategy, s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
form.isDraft, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
context.settings
) match { val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
case Right(objectId) => redirect(s"/${owner}/${name}/pull/${issueId}") pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
case Left(message) => Some(BadRequest(message))
// close issue by content of pull request
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
closeIssuesFromMessage(issue.title + " " + issue.content.getOrElse(""), loginAccount.userName, owner, name)
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
updatePullRequests(owner, name, pullreq.branch)
// call web hook
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
// call hooks
PluginRegistry().getPullRequestHooks.foreach{ h =>
h.addedComment(commentId, form.message, issue, repository)
h.merged(issue, repository)
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
}
} }
} getOrElse NotFound() } getOrElse NotFound()
}) })
get("/:owner/:repository/compare")(referrersOnly { forkedRepository => get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
val headBranch: Option[String] = params.get("head") val headBranch:Option[String] = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => { case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName).map { getRepository(originUserName, originRepositoryName).map { originRepository =>
originRepository => using(
Using.resources( Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) ){ (oldGit, newGit) =>
) { (oldGit, newGit) => val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2) val oldBranch = originRepository.branchList.find( _ == newBranch).getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
val oldBranch = originRepository.branchList
.find(_ == newBranch)
.getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
redirect( redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}" }
)
}
} getOrElse NotFound() } getOrElse NotFound()
} }
case _ => { case _ => {
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git => using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
case (_, defaultBranch) => redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}")
redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}"
)
} getOrElse { } getOrElse {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}") redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
} }
@@ -414,90 +327,84 @@ trait PullRequestsControllerBase extends ControllerBase {
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner) val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for (originRepositoryName <- if (originOwner == forkedOwner) { (for(
// Self repository originRepositoryName <- if(originOwner == forkedOwner) {
Some(forkedRepository.name) // Self repository
} else if (forkedRepository.repository.originUserName.isEmpty) { Some(forkedRepository.name)
// when ForkedRepository is the original repository } else if(forkedRepository.repository.originUserName.isEmpty){
getForkedRepositories(forkedRepository.owner, forkedRepository.name) // when ForkedRepository is the original repository
.find(_.userName == originOwner) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
.map(_.repositoryName) } else if(Some(originOwner) == forkedRepository.repository.originUserName){
} else if (Some(originOwner) == forkedRepository.repository.originUserName) { // Original repository
// Original repository forkedRepository.repository.originRepositoryName
forkedRepository.repository.originRepositoryName } else {
} else { // Sibling repository
// Sibling repository getUserRepositories(originOwner).find { x =>
getUserRepositories(originOwner) x.repository.originUserName == forkedRepository.repository.originUserName &&
.find { x => x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
x.repository.originUserName == forkedRepository.repository.originUserName && }.map(_.repository.repositoryName)
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName };
} originRepository <- getRepository(originOwner, originRepositoryName)
.map(_.repository.repositoryName) ) yield {
}; using(
originRepository <- getRepository(originOwner, originRepositoryName)) yield { Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
val (oldId, newId) = Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId) ){ case (oldGit, newGit) =>
val (oldId, newId) =
if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
// Branch name
val rootId = JGitUtil.getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originId,
forkedRepository.owner, forkedRepository.name, forkedId)
(oldId, newId) match { (Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
case (Some(oldId), Some(newId)) => {
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner,
originRepository.name,
oldId.getName,
forkedRepository.owner,
forkedRepository.name,
newId.getName
)
val title = if (commits.flatten.length == 1) {
commits.flatten.head.shortMessage
} else { } else {
val text = forkedId.replaceAll("[\\-_]", " ") // Commit id
text.substring(0, 1).toUpperCase + text.substring(1) (Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
} }
html.compare( (oldId, newId) match {
title, case (Some(oldId), Some(newId)) => {
commits, val (commits, diffs) = getRequestCompareInfo(
diffs, originRepository.owner, originRepository.name, oldId.getName,
((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { forkedRepository.owner, forkedRepository.name, newId.getName)
case (Some(userName), Some(repositoryName)) =>
getRepository(userName, repositoryName) match {
case Some(x) => x.repository :: getForkedRepositories(userName, repositoryName)
case None => getForkedRepositories(userName, repositoryName)
}
case _ =>
forkedRepository.repository :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
}).map { repository =>
(repository.userName, repository.repositoryName, repository.defaultBranch)
},
commits.flatten
.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false))
.flatten
.toList,
originId,
forkedId,
oldId.getName,
newId.getName,
getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"),
forkedRepository,
originRepository,
forkedRepository,
hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount),
getAssignableUserNames(originRepository.owner, originRepository.name),
getMilestones(originRepository.owner, originRepository.name),
getPriorities(originRepository.owner, originRepository.name),
getDefaultPriority(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name)
)
}
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)}"
)
val title = if(commits.flatten.length == 1){
commits.flatten.head.shortMessage
} else {
val text = forkedId.replaceAll("[\\-_]", " ")
text.substring(0, 1).toUpperCase + text.substring(1)
}
html.compare(
title,
commits,
diffs,
((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
}).filter { case (owner, name) => hasGuestRole(owner, name, context.loginAccount) },
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
originId,
forkedId,
oldId.getName,
newId.getName,
getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"),
forkedRepository,
originRepository,
forkedRepository,
hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount),
getAssignableUserNames(originRepository.owner, originRepository.name),
getMilestones(originRepository.owner, originRepository.name),
getPriorities(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name)
)
}
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)}")
}
} }
}) getOrElse NotFound() }) getOrElse NotFound()
}) })
@@ -507,152 +414,126 @@ trait PullRequestsControllerBase extends ControllerBase {
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner)
(for (originRepositoryName <- if (originOwner == forkedOwner) { (for(
Some(forkedRepository.name) originRepositoryName <- if(originOwner == forkedOwner){
} else { Some(forkedRepository.name)
forkedRepository.repository.originRepositoryName.orElse { } else {
getForkedRepositories(forkedRepository.owner, forkedRepository.name) forkedRepository.repository.originRepositoryName.orElse {
.find(_.userName == originOwner) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
.map(_.repositoryName) }
} };
}; originRepository <- getRepository(originOwner, originRepositoryName)
originRepository <- getRepository(originOwner, originRepositoryName)) yield { ) yield {
Using.resources( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
) { ){ case (oldGit, newGit) =>
case (oldGit, newGit) => val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}"){
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}") { checkConflict(originRepository.owner, originRepository.name, originBranch,
checkConflict( forkedRepository.owner, forkedRepository.name, forkedBranch)
originRepository.owner, }
originRepository.name, html.mergecheck(conflict)
originBranch,
forkedRepository.owner,
forkedRepository.name,
forkedBranch
)
}
html.mergecheck(conflict.isDefined)
} }
}) getOrElse NotFound() }) getOrElse NotFound()
}) })
post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, name) =>
case (owner, name) => val manageable = isManageable(repository)
val manageable = isManageable(repository) val loginUserName = context.loginAccount.get.userName
val loginUserName = context.loginAccount.get.userName
val issueId = insertIssue( val issueId = insertIssue(
owner = repository.owner, owner = repository.owner,
repository = repository.name, repository = repository.name,
loginUser = loginUserName, loginUser = loginUserName,
title = form.title, title = form.title,
content = form.content, content = form.content,
assignedUserName = if (manageable) form.assignedUserName else None, assignedUserName = if (manageable) form.assignedUserName else None,
milestoneId = if (manageable) form.milestoneId else None, milestoneId = if (manageable) form.milestoneId else None,
priorityId = if (manageable) form.priorityId else None, priorityId = if (manageable) form.priorityId else None,
isPullRequest = true isPullRequest = true)
)
createPullRequest( createPullRequest(
originRepository = repository, originUserName = repository.owner,
issueId = issueId, originRepositoryName = repository.name,
originBranch = form.targetBranch, issueId = issueId,
requestUserName = form.requestUserName, originBranch = form.targetBranch,
requestRepositoryName = form.requestRepositoryName, requestUserName = form.requestUserName,
requestBranch = form.requestBranch, requestRepositoryName = form.requestRepositoryName,
commitIdFrom = form.commitIdFrom, requestBranch = form.requestBranch,
commitIdTo = form.commitIdTo, commitIdFrom = form.commitIdFrom,
isDraft = form.isDraft, commitIdTo = form.commitIdTo)
loginAccount = context.loginAccount.get,
settings = context.settings
)
// insert labels // insert labels
if (manageable) { if (manageable) {
form.labelNames.foreach { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label => labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId) registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
}
} }
} }
} }
redirect(s"/${owner}/${name}/pull/${issueId}")
}
})
ajaxGet("/:owner/:repository/pulls/proposals")(readableUsersOnly { repository =>
val thresholdTime = System.currentTimeMillis() - (1000 * 60 * 60)
val mailAddresses =
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
} }
val targetRepository = (for { // fetch requested branch
parentUserName <- repository.repository.parentUserName fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
parentRepoName <- repository.repository.parentRepositoryName
parentRepository <- getRepository(parentUserName, parentRepoName)
} yield {
parentRepository
}).getOrElse {
repository
}
val proposedBranches = branches.filter { branch => // record activity
getPullRequestsByRequest(repository.owner, repository.name, branch, None).isEmpty recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
}
html.proposals(proposedBranches, targetRepository, repository) // call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
getIssue(owner, name, issueId.toString) foreach { issue =>
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// call hooks
PluginRegistry().getPullRequestHooks.foreach(_.created(issue, repository))
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
}) })
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifier(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))
} else {
(defaultOwner, value)
}
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name) { defining(repository.owner, repository.name){ case (owner, repoName) =>
case (owner, repoName) => val page = IssueSearchCondition.page(request)
val page = IssueSearchCondition.page(request)
// retrieve search condition // retrieve search condition
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
gitbucket.core.issues.html.list( gitbucket.core.issues.html.list(
"pulls", "pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page, page,
getAssignableUserNames(owner, repoName), getAssignableUserNames(owner, repoName),
getMilestones(owner, repoName), getMilestones(owner, repoName),
getPriorities(owner, repoName), getPriorities(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), true, owner -> repoName), countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName), countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition, condition,
repository, repository,
isEditable(repository), isEditable(repository),
isManageable(repository) isManageable(repository))
)
} }
/** /**

View File

@@ -1,233 +0,0 @@
package gitbucket.core.controller
import java.io.File
import gitbucket.core.service.{AccountService, ActivityService, PaginationHelper, ReleaseService, RepositoryService}
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
import scala.util.Using
class ReleaseController
extends ReleaseControllerBase
with RepositoryService
with AccountService
with ReleaseService
with ActivityService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
trait ReleaseControllerBase extends ControllerBase {
self: RepositoryService
with AccountService
with ReleaseService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
with ActivityService =>
case class ReleaseForm(
name: String,
content: Option[String]
)
val releaseForm = mapping(
"name" -> trim(text(required)),
"content" -> trim(optional(text()))
)(ReleaseForm.apply)
get("/:owner/:repository/releases")(referrersOnly { repository =>
val page = PaginationHelper.page(params.get("page"))
html.list(
repository,
fetchReleases(repository, page),
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
page,
repository.tags.size
)
})
get("/:owner/:repository/releases/:tag")(referrersOnly { repository =>
val tagName = params("tag")
getRelease(repository.owner, repository.name, tagName)
.map { release =>
html.release(
release,
getReleaseAssets(repository.owner, repository.name, tagName),
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
repository
)
}
.getOrElse(NotFound())
})
get("/:owner/:repository/releases/:tag/assets/:fileId")(referrersOnly { repository =>
val tagName = params("tag")
val fileId = params("fileId")
(for {
_ <- repository.tags.find(_.name == tagName)
_ <- getRelease(repository.owner, repository.name, tagName)
asset <- getReleaseAsset(repository.owner, repository.name, tagName, fileId)
} yield {
response.setHeader("Content-Disposition", s"attachment; filename=${asset.label}")
RawData(
FileUtil.getSafeMimeType(asset.label),
new File(getReleaseFilesDir(repository.owner, repository.name), FileUtil.checkFilename(tagName + "/" + fileId))
)
}).getOrElse(NotFound())
})
get("/:owner/:repository/releases/:tag/create")(writableUsersOnly { repository =>
val tagName = params("tag")
val previousTags = repository.tags.takeWhile(_.name != tagName).reverse
repository.tags
.find(_.name == tagName)
.map { tag =>
html.form(repository, tag, previousTags.map(_.name), tag.message, None)
}
.getOrElse(NotFound())
})
post("/:owner/:repository/releases/:tag/create", releaseForm)(writableUsersOnly { (form, repository) =>
val tagName = params("tag")
val loginAccount = context.loginAccount.get
// 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)
}
recordReleaseActivity(repository.owner, repository.name, loginAccount.userName, form.name, tagName)
redirect(s"/${repository.owner}/${repository.name}/releases/${tagName}")
})
get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository =>
val Seq(previousTag, currentTag) = multiParams("splat")
val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.id }.getOrElse("")
val 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}"
}
.mkString("\n")
}
commitLog
})
get("/:owner/:repository/releases/:tag/edit")(writableUsersOnly { repository =>
val tagName = params("tag")
val previousTags = repository.tags.takeWhile(_.name != tagName).reverse
(for {
release <- getRelease(repository.owner, repository.name, tagName)
tag <- repository.tags.find(_.name == tagName)
} yield {
html.form(
repository,
tag,
previousTags.map(_.name),
release.content.getOrElse(""),
Some(release, getReleaseAssets(repository.owner, repository.name, tagName))
)
}).getOrElse(NotFound())
})
post("/:owner/:repository/releases/:tag/edit", releaseForm)(writableUsersOnly {
(form, repository) =>
val tagName = params("tag")
val loginAccount = context.loginAccount.get
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)
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}")
}
.getOrElse(NotFound())
})
post("/:owner/:repository/releases/:tag/delete")(writableUsersOnly { repository =>
val tagName = params("tag")
getRelease(repository.owner, repository.name, tagName).foreach { release =>
FileUtils.deleteDirectory(
new File(getReleaseFilesDir(repository.owner, repository.name), FileUtil.checkFilename(release.tag))
)
}
deleteRelease(repository.owner, repository.name, tagName)
redirect(s"/${repository.owner}/${repository.name}/releases")
})
private def fetchReleases(repository: RepositoryService.RepositoryInfo, page: Int) = {
import gitbucket.core.service.ReleaseService._
val (offset, limit) = ((page - 1) * ReleaseLimit, ReleaseLimit)
val tagsToDisplay = repository.tags.reverse.slice(offset, offset + limit)
val releases = getReleases(repository.owner, repository.name, tagsToDisplay)
val assets = getReleaseAssetsMap(repository.owner, repository.name, releases)
val tagsWithReleases = tagsToDisplay.map { tag =>
(tag, releases.find(_.tag == tag.name).map { release =>
(release, assets(release))
})
}
tagsWithReleases
}
}

View File

@@ -12,103 +12,75 @@ import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.model.WebHookContentType
import org.scalatra.forms._ import org.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import gitbucket.core.model.WebHookContentType
import gitbucket.core.plugin.PluginRegistry
import scala.util.Using
import org.scalatra.Forbidden
class RepositorySettingsController class RepositorySettingsController extends RepositorySettingsControllerBase
extends RepositorySettingsControllerBase with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService with DeployKeyService
with RepositoryService with OwnerAuthenticator with UsersAuthenticator
with AccountService
with WebHookService
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with OwnerAuthenticator
with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase { trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService with DeployKeyService
with AccountService with OwnerAuthenticator with UsersAuthenticator =>
with WebHookService
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with OwnerAuthenticator
with UsersAuthenticator =>
// for repository options // for repository options
case class OptionsForm( case class OptionsForm(
repositoryName: String,
description: Option[String], description: Option[String],
isPrivate: Boolean, isPrivate: Boolean,
issuesOption: String, issuesOption: String,
externalIssuesUrl: Option[String], externalIssuesUrl: Option[String],
wikiOption: String, wikiOption: String,
externalWikiUrl: Option[String], externalWikiUrl: Option[String],
allowFork: Boolean, allowFork: Boolean
mergeOptions: Seq[String],
defaultMergeOption: String
) )
val optionsForm = mapping( val optionsForm = mapping(
"description" -> trim(label("Description", optional(text()))), "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), repository, renameRepositoryName))),
"isPrivate" -> trim(label("Repository Type", boolean())), "description" -> trim(label("Description" , optional(text()))),
"issuesOption" -> trim(label("Issues Option", text(required, featureOption))), "isPrivate" -> trim(label("Repository Type" , boolean())),
"issuesOption" -> trim(label("Issues Option" , text(required, featureOption))),
"externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))), "externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))),
"wikiOption" -> trim(label("Wiki Option", text(required, featureOption))), "wikiOption" -> trim(label("Wiki Option" , text(required, featureOption))),
"externalWikiUrl" -> trim(label("External Wiki URL", optional(text(maxlength(200))))), "externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))),
"allowFork" -> trim(label("Allow Forking", boolean())), "allowFork" -> trim(label("Allow Forking" , boolean()))
"mergeOptions" -> mergeOptions, )(OptionsForm.apply)
"defaultMergeOption" -> trim(label("Default merge strategy", text(required)))
)(OptionsForm.apply).verifying { form =>
if (!form.mergeOptions.contains(form.defaultMergeOption)) {
Seq("defaultMergeOption" -> s"This merge strategy isn't enabled.")
} else Nil
}
// for default branch // for default branch
case class DefaultBranchForm(defaultBranch: String) case class DefaultBranchForm(defaultBranch: String)
val defaultBranchForm = mapping( val defaultBranchForm = mapping(
"defaultBranch" -> trim(label("Default Branch", text(required, maxlength(100)))) "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
)(DefaultBranchForm.apply) )(DefaultBranchForm.apply)
// for deploy key // for deploy key
case class DeployKeyForm(title: String, publicKey: String, allowWrite: Boolean) case class DeployKeyForm(title: String, publicKey: String, allowWrite: Boolean)
val deployKeyForm = mapping( val deployKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))), "title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim2(label("Key", text(required))), // TODO duplication check in the repository? "publicKey" -> trim2(label("Key" , text(required))), // TODO duplication check in the repository?
"allowWrite" -> trim(label("Key", boolean())) "allowWrite" -> trim(label("Key" , boolean()))
)(DeployKeyForm.apply) )(DeployKeyForm.apply)
// for web hook url addition // for web hook url addition
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])
def webHookForm(update: Boolean) = def webHookForm(update:Boolean) = mapping(
mapping( "url" -> trim(label("url", text(required, webHook(update)))),
"url" -> trim(label("url", text(required, webHook(update)))), "events" -> webhookEvents,
"events" -> webhookEvents, "ctype" -> label("ctype", text()),
"ctype" -> label("ctype", text()), "token" -> optional(trim(label("token", text(maxlength(100)))))
"token" -> optional(trim(label("token", text(maxlength(100))))) )(
)( (url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
(url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token) )
)
// for rename repository
case class RenameRepositoryForm(repositoryName: String)
val renameForm = mapping(
"repositoryName" -> trim(
label("New repository name", text(required, maxlength(100), repository, renameRepositoryName))
)
)(RenameRepositoryForm.apply)
// for transfer ownership // for transfer ownership
case class TransferOwnerShipForm(newOwner: String) case class TransferOwnerShipForm(newOwner: String)
@@ -146,12 +118,35 @@ trait RepositorySettingsControllerBase extends ControllerBase {
form.externalIssuesUrl, form.externalIssuesUrl,
form.wikiOption, form.wikiOption,
form.externalWikiUrl, form.externalWikiUrl,
form.allowFork, form.allowFork
form.mergeOptions,
form.defaultMergeOption
) )
flash.update("info", "Repository settings has been updated.") // Change repository name
redirect(s"/${repository.owner}/${repository.name}/settings/options") if(repository.name != form.repositoryName){
// Update database
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
// Move git repository
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory){
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
}
}
// Move wiki repository
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
// Move files directory
defining(getRepositoryFilesDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getRepositoryFilesDir(repository.owner, form.repositoryName))
}
}
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.renamed(repository.owner, repository.name, form.repositoryName))
}
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
}) })
/** branch settings */ /** branch settings */
@@ -162,15 +157,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** Update default branch */ /** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
if (!repository.branchList.contains(form.defaultBranch)) { if(!repository.branchList.contains(form.defaultBranch)){
redirect(s"/${repository.owner}/${repository.name}/settings/options") redirect(s"/${repository.owner}/${repository.name}/settings/options")
} else { } else {
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch) saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
// Change repository HEAD // Change repository HEAD
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + form.defaultBranch) git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + form.defaultBranch)
} }
flash.update("info", "Repository default branch has been updated.") flash += "info" -> "Repository default branch has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/branches") redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} }
}) })
@@ -179,15 +174,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository => get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._ import gitbucket.core.api._
val branch = params("branch") val branch = params("branch")
if (!repository.branchList.contains(branch)) { if(!repository.branchList.contains(branch)){
redirect(s"/${repository.owner}/${repository.name}/settings/branches") redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else { } else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatuesContexts( val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name,
repository.owner, Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.of("UTC")))).toSet
repository.name,
Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC))
).toSet
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity) val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
html.branchprotection(repository, branch, protection, knownContexts, flash.get("info")) html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
} }
@@ -200,14 +192,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
html.collaborators( html.collaborators(
getCollaborators(repository.owner, repository.name), getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount, getAccountByUserName(repository.owner).get.isGroupAccount,
repository repository)
)
}) })
post("/:owner/:repository/settings/collaborators")(ownerOnly { repository => post("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
val collaborators = params("collaborators") val collaborators = params("collaborators")
removeCollaborators(repository.owner, repository.name) removeCollaborators(repository.owner, repository.name)
collaborators.split(",").withFilter(_.nonEmpty).foreach { collaborator => collaborators.split(",").withFilter(_.nonEmpty).map { collaborator =>
val userName :: role :: Nil = collaborator.split(":").toList val userName :: role :: Nil = collaborator.split(":").toList
addCollaborator(repository.owner, repository.name, userName, role) addCollaborator(repository.owner, repository.name, userName, role)
} }
@@ -234,7 +225,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/ */
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) => post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
addWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token) addWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token)
flash.update("info", s"Webhook ${form.url} created") flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -243,7 +234,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/ */
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHook(repository.owner, repository.name, params("url")) deleteWebHook(repository.owner, repository.name, params("url"))
flash.update("info", s"Webhook ${params("url")} deleted") flash += "info" -> s"Webhook ${params("url")} deleted"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -251,91 +242,62 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Send the test request to registered web hook URLs. * Send the test request to registered web hook URLs.
*/ */
ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => Array(h.getName, h.getValue) }
Array(h.getName, h.getValue)
}
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git => import scala.collection.JavaConverters._
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent._ import scala.concurrent._
import scala.jdk.CollectionConverters._ import scala.util.control.NonFatal
import scala.util.control.NonFatal import org.apache.http.util.EntityUtils
import org.apache.http.util.EntityUtils import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global
val url = params("url") val url = params("url")
val token = Some(params("token")) val token = Some(params("token"))
val ctype = WebHookContentType.valueOf(params("ctype")) val ctype = WebHookContentType.valueOf(params("ctype"))
val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token) val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token)
val dummyPayload = { val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get val ownerAccount = getAccountByUserName(repository.owner).get
val commits = val commits = if(JGitUtil.isEmpty(git)) List.empty else git.log
if (JGitUtil.isEmpty(git)) List.empty .add(git.getRepository.resolve(repository.repository.defaultBranch))
else .setMaxCount(4)
git.log .call.iterator.asScala.map(new CommitInfo(_)).toList
.add(git.getRepository.resolve(repository.repository.defaultBranch)) val pushedCommit = commits.drop(1)
.setMaxCount(4)
.call
.iterator
.asScala
.map(new CommitInfo(_))
.toList
val pushedCommit = commits.drop(1)
WebHookPushPayload( WebHookPushPayload(
git = git, git = git,
sender = ownerAccount, sender = ownerAccount,
refName = "refs/heads/" + repository.repository.defaultBranch, refName = "refs/heads/" + repository.repository.defaultBranch,
repositoryInfo = repository, repositoryInfo = repository,
commits = pushedCommit, commits = pushedCommit,
repositoryOwner = ownerAccount, repositoryOwner = ownerAccount,
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()), oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()) newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId())
)
}
val (webHook, json, reqFuture, resFuture) =
callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload, context.settings).head
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(),
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
)
)
.recover(toErrorMap),
20 seconds
)
)
) )
}
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
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"-> (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(),
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
)).recover(toErrorMap), 20 seconds)
))
} }
}) })
@@ -343,9 +305,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook edit page. * Display the web hook edit page.
*/ */
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map { getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
case (webhook, events) => html.edithook(webhook, events, repository, false)
html.edithook(webhook, events, repository, false)
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -354,7 +315,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/ */
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) => post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
updateWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token) updateWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token)
flash.update("info", s"webhook ${form.url} updated") flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -365,40 +326,59 @@ trait RepositorySettingsControllerBase extends ControllerBase {
html.danger(_, flash.get("info")) html.danger(_, flash.get("info"))
}) })
/**
* Rename repository.
*/
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
if (context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin) {
if (repository.name != form.repositoryName) {
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
}
redirect(s"/${repository.owner}/${form.repositoryName}")
} else Forbidden()
})
/** /**
* Transfer repository ownership. * Transfer repository ownership.
*/ */
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) { // Change repository owner
// Change repository owner if(repository.owner != form.newOwner){
if (repository.owner != form.newOwner) { LockUtil.lock(s"${repository.owner}/${repository.name}"){
// Update database
renameRepository(repository.owner, repository.name, form.newOwner, repository.name) renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Move git repository
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory){
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
}
}
// Move wiki repository
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
}
}
// Move files directory
defining(getRepositoryFilesDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getRepositoryFilesDir(form.newOwner, repository.name))
}
}
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.transferred(repository.owner, form.newOwner, repository.name))
} }
redirect(s"/${form.newOwner}/${repository.name}") }
} else Forbidden() redirect(s"/${form.newOwner}/${repository.name}")
}) })
/** /**
* Delete the repository. * Delete the repository.
*/ */
post("/:owner/:repository/settings/delete")(ownerOnly { repository => post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
if (context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin) { LockUtil.lock(s"${repository.owner}/${repository.name}"){
// Delete the repository and related files // Delete the repository and related files
deleteRepository(repository.repository) deleteRepository(repository.owner, repository.name)
redirect(s"/${repository.owner}")
} else Forbidden() FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getRepositoryFilesDir(repository.owner, repository.name))
// Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.deleted(repository.owner, repository.name))
}
redirect(s"/${repository.owner}")
}) })
/** /**
@@ -406,11 +386,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/ */
post("/:owner/:repository/settings/gc")(ownerOnly { repository => post("/:owner/:repository/settings/gc")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}") { LockUtil.lock(s"${repository.owner}/${repository.name}") {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.gc().call() git.gc().call()
} }
} }
flash.update("info", "Garbage collection has been executed.") flash += "info" -> "Garbage collection has been executed."
redirect(s"/${repository.owner}/${repository.name}/settings/danger") redirect(s"/${repository.owner}/${repository.name}/settings/danger")
}) })
@@ -435,10 +415,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** /**
* Provides duplication check for web hook url. * Provides duplication check for web hook url.
*/ */
private def webHook(needExists: Boolean): Constraint = new Constraint() { private def webHook(needExists: Boolean): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) { if(getWebHook(params("owner"), params("repository"), value).isDefined != needExists){
Some(if (needExists) { Some(if(needExists){
"URL had not been registered yet." "URL had not been registered yet."
} else { } else {
"URL had been registered already." "URL had been registered already."
@@ -448,18 +428,17 @@ trait RepositorySettingsControllerBase extends ControllerBase {
} }
} }
private def webhookEvents = new ValueType[Set[WebHook.Event]] { private def webhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = { def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t => WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t) params.get(name + "." + t.name).map(_ => t)
}.toSet }.toSet
} }
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
if (convert(name, params, messages).isEmpty) { Seq(name -> messages("error.required").format(name))
Seq(name -> messages("error.required").format(name)) } else {
} else { Nil
Nil }
}
} }
// /** // /**
@@ -480,17 +459,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** /**
* Duplicate check for the rename repository name. * Duplicate check for the rename repository name.
*/ */
private def renameRepositoryName: Constraint = new Constraint() { private def renameRepositoryName: Constraint = new Constraint(){
override def validate( override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = {
name: String,
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] = {
for { for {
repoName <- params.optionValue("repository") if repoName != value repoName <- params.optionValue("repository") if repoName != value
userName <- params.optionValue("owner") userName <- params.optionValue("owner")
_ <- getRepositoryNamesOfUser(userName).find(_ == value) _ <- getRepositoryNamesOfUser(userName).find(_ == value)
} yield { } yield {
"Repository already exists." "Repository already exists."
} }
@@ -500,50 +474,26 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** /**
* *
*/ */
private def featureOption: Constraint = new Constraint() { private def featureOption: Constraint = new Constraint(){
override def validate( override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] =
name: String, if(Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
value: String,
params: Map[String, Seq[String]],
messages: Messages
): Option[String] =
if (Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
} }
/** /**
* Provides Constraint to validate the repository transfer user. * Provides Constraint to validate the repository transfer user.
*/ */
private def transferUser: Constraint = new Constraint() { private def transferUser: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match { getAccountByUserName(value) match {
case None => Some("User does not exist.") case None => Some("User does not exist.")
case Some(x) => case Some(x) => if(x.userName == params("owner")){
if (x.userName == params("owner")) { Some("This is current repository owner.")
Some("This is current repository owner.") } else {
} else { params.get("repository").flatMap { repositoryName =>
params.get("repository").flatMap { repositoryName => getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map { _ =>
"User already has same repository."
}
}
} }
}
} }
} }
private def mergeOptions = new ValueType[Seq[String]] {
override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = {
params.getOrElse("mergeOptions", Nil)
}
override def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = {
val mergeOptions = params.getOrElse("mergeOptions", Nil)
if (mergeOptions.isEmpty) {
Seq("mergeOptions" -> "At least one option must be enabled.")
} else if (!mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))) {
Seq("mergeOptions" -> "mergeOptions are invalid.")
} else {
Nil
}
}
}
} }

View File

@@ -3,134 +3,89 @@ package gitbucket.core.controller
import java.io.FileInputStream import java.io.FileInputStream
import gitbucket.core.admin.html import gitbucket.core.admin.html
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.service.{AccountService, RepositoryService, 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.SyntaxSugars._
import gitbucket.core.util.{AdminAuthenticator, Mailer} import gitbucket.core.util.{AdminAuthenticator, Mailer}
import org.apache.commons.io.IOUtils import gitbucket.core.ssh.SshServer
import org.apache.commons.mail.EmailException import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
import org.json4s.jackson.Serialization import SystemSettingsService._
import org.scalatra._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.StringUtil._
import org.scalatra.forms._ import org.scalatra.forms._
import org.apache.commons.io.{FileUtils, IOUtils}
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import com.github.zafarkhaja.semver.{Version => Semver}
import gitbucket.core.GitBucketCoreModule
import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer
import scala.util.Using
class SystemSettingsController class SystemSettingsController extends SystemSettingsControllerBase
extends SystemSettingsControllerBase with AccountService with RepositoryService with AdminAuthenticator
with AccountService
with RepositoryService
with AdminAuthenticator
case class Table(name: String, columns: Seq[Column])
case class Column(name: String, primaryKey: Boolean)
trait SystemSettingsControllerBase extends AccountManagementControllerBase { trait SystemSettingsControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with AdminAuthenticator => self: AccountService with RepositoryService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))), "information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())), "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())), "isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
"repositoryOperation" -> mapping( "gravatar" -> trim(label("Gravatar", boolean())),
"create" -> trim(label("Allow all users to create repository", boolean())), "notification" -> trim(label("Notification", boolean())),
"delete" -> trim(label("Allow all users to delete repository", boolean())), "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"rename" -> trim(label("Allow all users to rename repository", boolean())), "ssh" -> trim(label("SSH access", boolean())),
"transfer" -> trim(label("Allow all users to transfer repository", boolean())), "sshHost" -> trim(label("SSH host", optional(text()))),
"fork" -> trim(label("Allow all users to fork repository", boolean())) "sshPort" -> trim(label("SSH port", optional(number()))),
)(RepositoryOperation.apply), "useSMTP" -> trim(label("SMTP", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "smtp" -> optionalIfNotChecked("useSMTP", mapping(
"notification" -> trim(label("Notification", boolean())), "host" -> trim(label("SMTP Host", text(required))),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), "port" -> trim(label("SMTP Port", optional(number()))),
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), "user" -> trim(label("SMTP User", optional(text()))),
"ssh" -> mapping( "password" -> trim(label("SMTP Password", optional(text()))),
"enabled" -> trim(label("SSH access", boolean())), "ssl" -> trim(label("Enable SSL", optional(boolean()))),
"host" -> trim(label("SSH host", optional(text()))), "starttls" -> trim(label("Enable STARTTLS", optional(boolean()))),
"port" -> trim(label("SSH port", optional(number()))) "fromAddress" -> trim(label("FROM Address", optional(text()))),
)(Ssh.apply), "fromName" -> trim(label("FROM Name", optional(text())))
"useSMTP" -> trim(label("SMTP", boolean())), )(Smtp.apply)),
"smtp" -> optionalIfNotChecked( "ldapAuthentication" -> trim(label("LDAP", boolean())),
"useSMTP", "ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
mapping( "host" -> trim(label("LDAP host", text(required))),
"host" -> trim(label("SMTP Host", text(required))), "port" -> trim(label("LDAP port", optional(number()))),
"port" -> trim(label("SMTP Port", optional(number()))), "bindDN" -> trim(label("Bind DN", optional(text()))),
"user" -> trim(label("SMTP User", optional(text()))), "bindPassword" -> trim(label("Bind Password", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))), "baseDN" -> trim(label("Base DN", text(required))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))), "userNameAttribute" -> trim(label("User name attribute", text(required))),
"starttls" -> trim(label("Enable STARTTLS", optional(boolean()))), "additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))), "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text()))) "mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
)(Smtp.apply) "tls" -> trim(label("Enable TLS", optional(boolean()))),
), "ssl" -> trim(label("Enable SSL", optional(boolean()))),
"ldapAuthentication" -> trim(label("LDAP", boolean())), "keystore" -> trim(label("Keystore", optional(text())))
"ldap" -> optionalIfNotChecked( )(Ldap.apply)),
"ldapAuthentication", "skinName" -> trim(label("AdminLTE skin name", text(required)))
mapping(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"additionalFilterCondition" -> trim(label("Additional filter condition", optional(text()))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply)
),
"oidcAuthentication" -> trim(label("OIDC", boolean())),
"oidc" -> optionalIfNotChecked(
"oidcAuthentication",
mapping(
"issuer" -> trim(label("Issuer", text(required))),
"clientID" -> trim(label("Client ID", text(required))),
"clientSecret" -> trim(label("Client secret", text(required))),
"jwsAlgorithm" -> trim(label("Signature algorithm", optional(text())))
)(OIDC.apply)
),
"skinName" -> trim(label("AdminLTE skin name", text(required))),
"userDefinedCss" -> trim(label("User-defined CSS", optional(text()))),
"showMailAddress" -> trim(label("Show mail address", boolean())),
"webhook" -> mapping(
"blockPrivateAddress" -> trim(label("Block private address", boolean())),
"whitelist" -> trim(label("Whitelist", multiLineText()))
)(WebHook.apply),
"upload" -> mapping(
"maxFileSize" -> trim(label("Max file size", long(required))),
"timeout" -> trim(label("Timeout", long(required))),
"largeMaxFileSize" -> trim(label("Max file size for large file", long(required))),
"largeTimeout" -> trim(label("Timeout for large file", long(required)))
)(Upload.apply)
)(SystemSettings.apply).verifying { settings => )(SystemSettings.apply).verifying { settings =>
Vector( Vector(
if (settings.ssh.enabled && settings.baseUrl.isEmpty) { if(settings.ssh && settings.baseUrl.isEmpty){
Some("baseUrl" -> "Base URL is required if SSH access is enabled.") Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else None, } else None,
if (settings.ssh.enabled && settings.ssh.sshHost.isEmpty) { if(settings.ssh && settings.sshHost.isEmpty){
Some("sshHost" -> "SSH host is required if SSH access is enabled.") Some("sshHost" -> "SSH host is required if SSH access is enabled.")
} else None } else None
).flatten ).flatten
} }
private val sendMailForm = mapping( private val sendMailForm = mapping(
"smtp" -> mapping( "smtp" -> mapping(
"host" -> trim(label("SMTP Host", text(required))), "host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))), "port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))), "user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))), "password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))), "ssl" -> trim(label("Enable SSL", optional(boolean()))),
"starttls" -> trim(label("Enable STARTTLS", optional(boolean()))), "starttls" -> trim(label("Enable STARTTLS", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))), "fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text()))) "fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply), )(Smtp.apply),
"testAddress" -> trim(label("", text(required))) "testAddress" -> trim(label("", text(required)))
)(SendMailForm.apply) )(SendMailForm.apply)
@@ -139,169 +94,66 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
case class DataExportForm(tableNames: List[String]) case class DataExportForm(tableNames: List[String])
case class NewUserForm( case class NewUserForm(userName: String, password: String, fullName: String,
userName: String, mailAddress: String, isAdmin: Boolean,
password: String, description: Option[String], url: Option[String], fileId: Option[String])
fullName: String,
mailAddress: String,
extraMailAddresses: List[String],
isAdmin: Boolean,
description: Option[String],
url: Option[String],
fileId: Option[String]
)
case class EditUserForm( case class EditUserForm(userName: String, password: Option[String], fullName: String,
userName: String, mailAddress: String, isAdmin: Boolean, description: Option[String], url: Option[String],
password: Option[String], fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
fullName: String,
mailAddress: String,
extraMailAddresses: List[String],
isAdmin: Boolean,
description: Option[String],
url: Option[String],
fileId: Option[String],
clearImage: Boolean,
isRemoved: Boolean
)
case class NewGroupForm( case class NewGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String],
groupName: String, members: String)
description: Option[String],
url: Option[String], case class EditGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String],
fileId: Option[String], members: String, clearImage: Boolean, isRemoved: Boolean)
members: String
)
case class EditGroupForm(
groupName: String,
description: Option[String],
url: Option[String],
fileId: Option[String],
members: String,
clearImage: Boolean,
isRemoved: Boolean
)
val newUserForm = mapping( val newUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password", text(required, maxlength(20)))), "password" -> trim(label("Password" ,text(required, maxlength(20), password))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))), "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress()))), "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
"extraMailAddresses" -> list( "isAdmin" -> trim(label("User Type" ,boolean())),
trim(label("Additional Mail Address", text(maxlength(100), uniqueExtraMailAddress("userName")))) "description" -> trim(label("bio" ,optional(text()))),
), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"isAdmin" -> trim(label("User Type", boolean())), "fileId" -> trim(label("File ID" ,optional(text())))
"description" -> trim(label("bio", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID", optional(text())))
)(NewUserForm.apply) )(NewUserForm.apply)
val editUserForm = mapping( val editUserForm = mapping(
"userName" -> trim(label("Username", text(required, maxlength(100), identifier))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
"password" -> trim(label("Password", optional(text(maxlength(20))))), "password" -> trim(label("Password" ,optional(text(maxlength(20), password)))),
"fullName" -> trim(label("Full Name", text(required, maxlength(100)))), "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address", text(required, maxlength(100), uniqueMailAddress("userName")))), "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
"extraMailAddresses" -> list( "isAdmin" -> trim(label("User Type" ,boolean())),
trim(label("Additional Mail Address", text(maxlength(100), uniqueExtraMailAddress("userName")))) "description" -> trim(label("bio" ,optional(text()))),
), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"isAdmin" -> trim(label("User Type", boolean())), "fileId" -> trim(label("File ID" ,optional(text()))),
"description" -> trim(label("bio", optional(text()))), "clearImage" -> trim(label("Clear image" ,boolean())),
"url" -> trim(label("URL", optional(text(maxlength(200))))), "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
"fileId" -> trim(label("File ID", optional(text()))),
"clearImage" -> trim(label("Clear image", boolean())),
"removed" -> trim(label("Disable", boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"description" -> trim(label("Group description", optional(text()))), "description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID", optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members", text(required, members))) "members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply) )(NewGroupForm.apply)
val editGroupForm = mapping( val editGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"description" -> trim(label("Group description", optional(text()))), "description" -> trim(label("Group description", optional(text()))),
"url" -> trim(label("URL", optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID", optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members", text(required, members))), "members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image", boolean())), "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable", boolean())) "removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply) )(EditGroupForm.apply)
get("/admin/dbviewer")(adminOnly {
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")
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))
}
}
tables += Table(tableName.toUpperCase, columns.toSeq)
}
}
html.dbviewer(tables.toSeq)
})
post("/admin/dbviewer/_query")(adminOnly {
contentType = formats("json")
params.get("query").collectFirst {
case query if query.trim.nonEmpty =>
val trimmedQuery = query.trim
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)
}
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)))
}
}
} catch {
case e: Exception =>
Ok(Serialization.write(Map("type" -> "error", "message" -> e.toString)))
}
}
} getOrElse Ok(Serialization.write(Map("type" -> "error", "message" -> "query is empty")))
})
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
html.settings(flash.get("info")) html.system(flash.get("info"))
}) })
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
@@ -309,133 +161,163 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
if (form.sshAddress != context.settings.sshAddress) { if (form.sshAddress != context.settings.sshAddress) {
SshServer.stop() SshServer.stop()
for { for {
sshAddress <- form.sshAddress sshAddress <- form.sshAddress
baseUrl <- form.baseUrl baseUrl <- form.baseUrl
} SshServer.start(sshAddress, baseUrl) }
SshServer.start(sshAddress, baseUrl)
} }
flash.update("info", "System settings has been updated.") flash += "info" -> "System settings has been updated."
redirect("/admin/system") redirect("/admin/system")
}) })
post("/admin/system/sendmail", sendMailForm)(adminOnly { form => post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
try { try {
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send( new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send(
to = form.testAddress, to = form.testAddress,
subject = "Test message from GitBucket", subject = "Test message from GitBucket",
textMsg = "This is a test message from GitBucket.", textMsg = "This is a test message from GitBucket.",
htmlMsg = None, htmlMsg = None,
loginAccount = context.loginAccount loginAccount = context.loginAccount
) )
"Test mail has been sent to: " + form.testAddress "Test mail has been sent to: " + form.testAddress
} catch { } catch {
case e: EmailException => s"[Error] ${e.getCause}" case e: Exception => "[Error] " + e.toString
case e: Exception => s"[Error] ${e.toString}"
} }
}) })
get("/admin/plugins")(adminOnly { get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins(), flash.get("info")) // Installed plugins
val enabledPlugins = PluginRegistry().getPlugins()
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion)
// Plugins in the local repository
val repositoryPlugins = PluginRepository.getPlugins()
.filterNot { meta =>
enabledPlugins.exists { plugin => plugin.pluginId == meta.id &&
Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version))
}
}.map { meta =>
(meta, meta.versions.reverse.find { version => gitbucketVersion.satisfies(version.range) })
}.collect { case (meta, Some(version)) =>
new PluginInfoBase(
pluginId = meta.id,
pluginName = meta.name,
pluginVersion = version.version,
description = meta.description
)
}
// Merge
val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false))
html.plugins(plugins, flash.get("info"))
}) })
post("/admin/plugins/_reload")(adminOnly { post("/admin/plugins/_reload")(adminOnly {
PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn) PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn)
flash.update("info", "All plugins were reloaded.") flash += "info" -> "All plugins were reloaded."
redirect("/admin/plugins") redirect("/admin/plugins")
}) })
post("/admin/plugins/:pluginId/_uninstall")(adminOnly { post("/admin/plugins/:pluginId/:version/_uninstall")(adminOnly {
val pluginId = params("pluginId") val pluginId = params("pluginId")
val version = params("version")
if (PluginRegistry().getPlugins().exists(_.pluginId == pluginId)) { PluginRegistry().getPlugins()
PluginRegistry .collect { case plugin if (plugin.pluginId == pluginId && plugin.pluginVersion == version) => plugin }
.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn) .foreach { _ =>
flash.update("info", s"${pluginId} was uninstalled.") PluginRegistry.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
} flash += "info" -> s"${pluginId} was uninstalled."
}
redirect("/admin/plugins") redirect("/admin/plugins")
}) })
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
val pluginId = params("pluginId")
val version = params("version")
/// TODO!!!!
PluginRepository.getPlugins()
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version) )}
.foreach { case (meta, version) =>
version.foreach { version =>
// TODO Install version!
PluginRegistry.install(
new java.io.File(PluginHome, s".repository/${version.file}"),
request.getServletContext,
loadSystemSettings(),
request2Session(request).conn
)
flash += "info" -> s"${pluginId} was installed."
}
}
redirect("/admin/plugins")
})
get("/admin/users")(adminOnly { get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val includeGroups = params.get("includeGroups").map(_.toBoolean).getOrElse(false) val users = getAllUsers(includeRemoved)
val users = getAllUsers(includeRemoved, includeGroups) val members = users.collect { case account if(account.isGroupAccount) =>
val members = users.collect { account.userName -> getGroupMembers(account.userName).map(_.userName)
case account if (account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap }.toMap
html.userlist(users, members, includeRemoved, includeGroups) html.userlist(users, members, includeRemoved)
}) })
get("/admin/users/_newuser")(adminOnly { get("/admin/users/_newuser")(adminOnly {
html.user(None, Nil) html.user(None)
}) })
post("/admin/users/_newuser", newUserForm)(adminOnly { form => post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount( createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.description, form.url)
form.userName,
pbkdf2_sha256(form.password),
form.fullName,
form.mailAddress,
form.isAdmin,
form.description,
form.url
)
updateImage(form.userName, form.fileId, false) updateImage(form.userName, form.fileId, false)
updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != ""))
redirect("/admin/users") redirect("/admin/users")
}) })
get("/admin/users/:userName/_edituser")(adminOnly { get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName") val userName = params("userName")
val extraMails = getAccountExtraMailAddresses(userName) html.user(getAccountByUserName(userName, true), flash.get("error"))
html.user(getAccountByUserName(userName, true), extraMails, flash.get("error"))
}) })
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName, true).map { getAccountByUserName(userName, true).map { account =>
account => if(account.isAdmin && (form.isRemoved || !form.isAdmin) && isLastAdministrator(account)){
if (account.isAdmin && (form.isRemoved || !form.isAdmin) && isLastAdministrator(account)) { flash += "error" -> "Account can't be turned off because this is last one administrator."
flash.update("error", "Account can't be turned off because this is last one administrator.") redirect(s"/admin/users/${userName}/_edituser")
redirect(s"/admin/users/${userName}/_edituser") } else {
} else { if(form.isRemoved){
if (form.isRemoved) { // Remove repositories
// Remove repositories // getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// getRepositoryNamesOfUser(userName).foreach { repositoryName => // deleteRepository(userName, repositoryName)
// deleteRepository(userName, repositoryName) // FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) // FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) // FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) // }
// } // Remove from GROUP_MEMBER and COLLABORATOR
// Remove from GROUP_MEMBER and COLLABORATOR removeUserRelatedData(userName)
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
)
)
updateImage(userName, form.fileId, form.clearImage)
updateAccountExtraMailAddresses(userName, form.extraMailAddresses.filter(_ != ""))
// call hooks
if (form.isRemoved) PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
redirect("/admin/users")
} }
updateAccount(account.copy(
password = form.password.map(sha1).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)
// call hooks
if(form.isRemoved) PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
redirect("/admin/users")
}
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -445,47 +327,33 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.description, form.url) createGroup(form.groupName, form.description, form.url)
updateGroupMembers( updateGroupMembers(form.groupName, form.members.split(",").map {
form.groupName, _.split(":") match {
form.members case Array(userName, isManager) => (userName, isManager.toBoolean)
.split(",") }
.map { }.toList)
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}
.toList
)
updateImage(form.groupName, form.fileId, false) updateImage(form.groupName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
get("/admin/users/:groupName/_editgroup")(adminOnly { get("/admin/users/:groupName/_editgroup")(adminOnly {
defining(params("groupName")) { groupName => defining(params("groupName")){ groupName =>
html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName)) html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
} }
}) })
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining( defining(params("groupName"), form.members.split(",").map {
params("groupName"), _.split(":") match {
form.members case Array(userName, isManager) => (userName, isManager.toBoolean)
.split(",") }
.map { }.toList){ case (groupName, members) =>
_.split(":") match { getAccountByUserName(groupName, true).map { account =>
case Array(userName, isManager) => (userName, isManager.toBoolean) updateGroup(groupName, form.description, form.url, form.isRemoved)
}
}
.toList
) {
case (groupName, members) =>
getAccountByUserName(groupName, true).map {
account =>
updateGroup(groupName, form.description, form.url, form.isRemoved)
if (form.isRemoved) { if(form.isRemoved){
// Remove from GROUP_MEMBER // Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil) updateGroupMembers(form.groupName, Nil)
// // Remove repositories // // Remove repositories
// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => // getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
// deleteRepository(groupName, repositoryName) // deleteRepository(groupName, repositoryName)
@@ -493,9 +361,9 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
// FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) // FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) // FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
// } // }
} else { } else {
// Update GROUP_MEMBER // Update GROUP_MEMBER
updateGroupMembers(form.groupName, members) updateGroupMembers(form.groupName, members)
// // Update COLLABORATOR for group repositories // // Update COLLABORATOR for group repositories
// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => // getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
// removeCollaborators(form.groupName, repositoryName) // removeCollaborators(form.groupName, repositoryName)
@@ -503,12 +371,12 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
// addCollaborator(form.groupName, repositoryName, userName) // addCollaborator(form.groupName, repositoryName, userName)
// } // }
// } // }
} }
updateImage(form.groupName, form.fileId, form.clearImage) updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
} getOrElse NotFound() } getOrElse NotFound()
} }
}) })
@@ -526,44 +394,30 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName) response.setHeader("Content-Disposition", "attachment; filename=" + file.getName)
response.setContentLength(file.length.toInt) response.setContentLength(file.length.toInt)
Using.resource(new FileInputStream(file)) { in => using(new FileInputStream(file)){ in =>
IOUtils.copy(in, response.outputStream) IOUtils.copy(in, response.outputStream)
} }
() ()
}) })
private def multiLineText(constraints: Constraint*): SingleValueType[Seq[String]] = private def members: Constraint = new Constraint(){
new SingleValueType[Seq[String]](constraints: _*) { override def validate(name: String, value: String, messages: Messages): Option[String] = {
def convert(value: String, messages: Messages): Seq[String] = { if(value.split(",").exists {
if (value == null) { _.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
Nil }) None else Some("Must select one manager at least.")
} else {
value.split("\n").toIndexedSeq.map(_.trim)
}
}
} }
}
private def members: Constraint = protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
new Constraint() { override def validate(name: String, value: String, messages: Messages): Option[String] = {
override def validate(name: String, value: String, messages: Messages): Option[String] = { params.get(paramName).flatMap { userName =>
if (value.split(",").exists { if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean } Some("You can't disable your account yourself")
}) None else
else Some("Must select one manager at least.") None
}
}
protected def disableByNotYourself(paramName: String): Constraint =
new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if (userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
Some("You can't disable your account yourself")
else
None
}
} }
} }
}
} }

View File

@@ -14,58 +14,58 @@ trait ValidationSupport extends FormSupport { self: ServletBase with JacksonJson
def get[T](path: String, form: ValueType[T])(action: T => Any): Route = { def get[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form) registerValidate(path, form)
get(path) { get(path){
validate(form)(errors => BadRequest(), form => action(form)) validate(form)(errors => BadRequest(), form => action(form))
} }
} }
def post[T](path: String, form: ValueType[T])(action: T => Any): Route = { def post[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form) registerValidate(path, form)
post(path) { post(path){
validate(form)(errors => BadRequest(), form => action(form)) validate(form)(errors => BadRequest(), form => action(form))
} }
} }
def put[T](path: String, form: ValueType[T])(action: T => Any): Route = { def put[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form) registerValidate(path, form)
put(path) { put(path){
validate(form)(errors => BadRequest(), form => action(form)) validate(form)(errors => BadRequest(), form => action(form))
} }
} }
def delete[T](path: String, form: ValueType[T])(action: T => Any): Route = { def delete[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form) registerValidate(path, form)
delete(path) { delete(path){
validate(form)(errors => BadRequest(), form => action(form)) validate(form)(errors => BadRequest(), form => action(form))
} }
} }
def ajaxGet[T](path: String, form: ValueType[T])(action: T => Any): Route = { def ajaxGet[T](path: String, form: ValueType[T])(action: T => Any): Route = {
get(path) { get(path){
validate(form)(errors => ajaxError(errors), form => action(form)) validate(form)(errors => ajaxError(errors), form => action(form))
} }
} }
def ajaxPost[T](path: String, form: ValueType[T])(action: T => Any): Route = { def ajaxPost[T](path: String, form: ValueType[T])(action: T => Any): Route = {
post(path) { post(path){
validate(form)(errors => ajaxError(errors), form => action(form)) validate(form)(errors => ajaxError(errors), form => action(form))
} }
} }
def ajaxDelete[T](path: String, form: ValueType[T])(action: T => Any): Route = { def ajaxDelete[T](path: String, form: ValueType[T])(action: T => Any): Route = {
delete(path) { delete(path){
validate(form)(errors => ajaxError(errors), form => action(form)) validate(form)(errors => ajaxError(errors), form => action(form))
} }
} }
def ajaxPut[T](path: String, form: ValueType[T])(action: T => Any): Route = { def ajaxPut[T](path: String, form: ValueType[T])(action: T => Any): Route = {
put(path) { put(path){
validate(form)(errors => ajaxError(errors), form => action(form)) validate(form)(errors => ajaxError(errors), form => action(form))
} }
} }
private def registerValidate[T](path: String, form: ValueType[T]) = { private def registerValidate[T](path: String, form: ValueType[T]) = {
post(path.replaceFirst("/$", "") + "/validate") { post(path.replaceFirst("/$", "") + "/validate"){
contentType = "application/json" contentType = "application/json"
toJson(form.validate("", multiParams, messages)) toJson(form.validate("", multiParams, messages))
} }
@@ -84,9 +84,8 @@ trait ValidationSupport extends FormSupport { self: ServletBase with JacksonJson
* Converts errors to JSON. * Converts errors to JSON.
*/ */
private def toJson(errors: Seq[(String, String)]): JObject = private def toJson(errors: Seq[(String, String)]): JObject =
JObject(errors.map { JObject(errors.map { case (key, value) =>
case (key, value) => JField(key, JString(value))
JField(key, JString(value))
}.toList) }.toList)
} }

View File

@@ -13,62 +13,39 @@ import gitbucket.core.util.Directory._
import org.scalatra.forms._ import org.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import scala.util.Using
class WikiController class WikiController extends WikiControllerBase
extends WikiControllerBase with WikiService with RepositoryService with AccountService with ActivityService with WebHookService
with WikiService with ReadableUsersAuthenticator with ReferrerAuthenticator
with RepositoryService
with AccountService
with ActivityService
with WebHookService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase { trait WikiControllerBase extends ControllerBase {
self: WikiService self: WikiService with RepositoryService with AccountService with ActivityService with WebHookService
with RepositoryService with ReadableUsersAuthenticator with ReferrerAuthenticator =>
with AccountService
with ActivityService
with WebHookService
with ReadableUsersAuthenticator
with ReferrerAuthenticator =>
case class WikiPageEditForm( case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
pageName: String,
content: String,
message: Option[String],
currentPageName: String,
id: String
)
val newForm = mapping( val newForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pagename, unique))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content", text(required, conflictForNew))), "content" -> trim(label("Content" , text(required, conflictForNew))),
"message" -> trim(label("Message", optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name", text())), "currentPageName" -> trim(label("Current page name" , text())),
"id" -> trim(label("Latest commit id", text())) "id" -> trim(label("Latest commit id" , text()))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
val editForm = mapping( val editForm = mapping(
"pageName" -> trim(label("Page name", text(required, maxlength(40), pagename))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content", text(required, conflictForEdit))), "content" -> trim(label("Content" , text(required, conflictForEdit))),
"message" -> trim(label("Message", optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name", text(required))), "currentPageName" -> trim(label("Current page name" , text(required))),
"id" -> trim(label("Latest commit id", text(required))) "id" -> trim(label("Latest commit id" , text(required)))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page => getWikiPage(repository.owner, repository.name, "Home").map { page =>
html.page( html.page("Home", page, getWikiPageList(repository.owner, repository.name),
"Home", repository, isEditable(repository),
page,
getWikiPageList(repository.owner, repository.name),
repository,
isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar"), getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer") getWikiPage(repository.owner, repository.name, "_Footer"))
)
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
}) })
@@ -76,25 +53,20 @@ trait WikiControllerBase extends ControllerBase {
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
getWikiPage(repository.owner, repository.name, pageName).map { page => getWikiPage(repository.owner, repository.name, pageName).map { page =>
html.page( html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
pageName, repository, isEditable(repository),
page,
getWikiPageList(repository.owner, repository.name),
repository,
isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar"), getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer") getWikiPage(repository.owner, repository.name, "_Footer"))
)
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
}) })
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository, isEditable(repository)) case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository, isEditable(repository))
case Left(_) => NotFound() case Left(_) => NotFound()
} }
} }
}) })
@@ -103,161 +75,128 @@ trait WikiControllerBase extends ControllerBase {
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare( html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true, false).filter(_.newPath == pageName + ".md"), repository,
Some(pageName), isEditable(repository), flash.get("info"))
from,
to,
JGitUtil.getDiffs(git, Some(from), to, true, false).filter(_.newPath == pageName + ".md"),
repository,
isEditable(repository),
flash.get("info")
)
} }
}) })
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare( html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true, false), repository,
None, isEditable(repository), flash.get("info"))
from,
to,
JGitUtil.getDiffs(git, Some(from), to, true, false),
repository,
isEditable(repository),
flash.get("info")
)
} }
}) })
get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository => get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository =>
if (isEditable(repository)) { if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
if (revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))) { if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}") redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
} else { } else {
flash.update("info", "This patch was not able to be reversed.") flash += "info" -> "This patch was not able to be reversed."
redirect( redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}"
)
} }
} else Unauthorized() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository => get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository =>
if (isEditable(repository)) { if(isEditable(repository)){
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
if (revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)) { if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
redirect(s"/${repository.owner}/${repository.name}/wiki") redirect(s"/${repository.owner}/${repository.name}/wiki/")
} else { } else {
flash.update("info", "This patch was not able to be reversed.") flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}") redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
} }
} else Unauthorized() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository => get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository =>
if (isEditable(repository)) { if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
} else Unauthorized() } else Unauthorized()
}) })
post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) =>
if (isEditable(repository)) { if(isEditable(repository)){
defining(context.loginAccount.get) { defining(context.loginAccount.get){ loginAccount =>
loginAccount => saveWikiPage(
saveWikiPage( repository.owner,
repository.owner, repository.name,
repository.name, form.currentPageName,
form.currentPageName, form.pageName,
form.pageName, appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"), loginAccount,
loginAccount, form.message.getOrElse(""),
form.message.getOrElse(""), Some(form.id)
Some(form.id) ).map { commitId =>
).foreach { updateLastActivityDate(repository.owner, repository.name)
commitId => recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
updateLastActivityDate(repository.owner, repository.name) callWebHookOf(repository.owner, repository.name, WebHook.Gollum){
recordEditWikiPageActivity( getAccountByUserName(repository.owner).map { repositoryUser =>
repository.owner, WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount)
repository.name, }
loginAccount.userName,
form.pageName,
commitId
)
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")
} }
}
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() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/_new")(readableUsersOnly { repository => get("/:owner/:repository/wiki/_new")(readableUsersOnly { repository =>
if (isEditable(repository)) { if(isEditable(repository)){
html.edit("", None, repository) html.edit("", None, repository)
} else Unauthorized() } else Unauthorized()
}) })
post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) =>
if (isEditable(repository)) { if(isEditable(repository)){
defining(context.loginAccount.get) { defining(context.loginAccount.get){ loginAccount =>
loginAccount => saveWikiPage(
saveWikiPage( repository.owner,
repository.owner, repository.name,
repository.name, form.currentPageName,
form.currentPageName, form.pageName,
form.pageName, form.content,
form.content, loginAccount,
loginAccount, form.message.getOrElse(""),
form.message.getOrElse(""), None
None ).map { commitId =>
).foreach { updateLastActivityDate(repository.owner, repository.name)
commitId => recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
updateLastActivityDate(repository.owner, repository.name) callWebHookOf(repository.owner, repository.name, WebHook.Gollum){
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) getAccountByUserName(repository.owner).map { repositoryUser =>
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) { WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount)
getAccountByUserName(repository.owner).map { repositoryUser => }
WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount)
}
}
} }
}
if (notReservedPageName(form.pageName)) { if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else { } else {
redirect(s"/${repository.owner}/${repository.name}/wiki") redirect(s"/${repository.owner}/${repository.name}/wiki")
} }
} }
} else Unauthorized() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository => get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository =>
if (isEditable(repository)) { if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
defining(context.loginAccount.get) { loginAccount => defining(context.loginAccount.get){ loginAccount =>
deleteWikiPage( deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}")
repository.owner,
repository.name,
pageName,
loginAccount.fullName,
loginAccount.mailAddress,
s"Destroyed ${pageName}"
)
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki") redirect(s"/${repository.owner}/${repository.name}/wiki")
@@ -270,51 +209,41 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match { JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository)) case Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository))
case Left(_) => NotFound() case Left(_) => NotFound()
} }
} }
}) })
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
val path = multiParams("splat").head val path = multiParams("splat").head
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve("master"))
getPathObjectId(git, path, revCommit).map { objectId => getFileContent(repository.owner, repository.name, path).map { bytes =>
responseRawFile(git, objectId, path, repository) RawData(FileUtil.getContentType(path, bytes), bytes)
} getOrElse NotFound() } getOrElse NotFound()
}
}) })
private def unique: Constraint = new Constraint() { private def unique: Constraint = new Constraint(){
override def validate( override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] =
name: String, getWikiPageList(params.value("owner"), params.value("repository")).find(_ == value).map(_ => "Page already exists.")
value: String,
params: Map[String, Seq[String]],
messages: Messages
): 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] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if (value.exists("\\/:*?\"<>|".contains(_))) { if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if (notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))) { } else if(notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))){
Some(s"${name} starts with invalid character.") Some(s"${name} starts with invalid character.")
} else { } else {
None None
} }
} }
private def notReservedPageName(value: String) = !(Array[String]("_Sidebar", "_Footer") contains value) private def notReservedPageName(value: String) = ! (Array[String]("_Sidebar","_Footer") contains value)
private def conflictForNew: Constraint = new Constraint() { private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
targetWikiPage.map { _ => targetWikiPage.map { _ =>
"Someone has created the wiki since you started. Please reload this page and re-apply your changes." "Someone has created the wiki since you started. Please reload this page and re-apply your changes."
@@ -322,9 +251,9 @@ trait WikiControllerBase extends ControllerBase {
} }
} }
private def conflictForEdit: Constraint = new Constraint() { private def conflictForEdit: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
targetWikiPage.filter(_.id != params("id")).map { _ => targetWikiPage.filter(_.id != params("id")).map{ _ =>
"Someone has edited the wiki since you started. Please reload this page and re-apply your changes." "Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
} }
} }

View File

@@ -1,75 +0,0 @@
package gitbucket.core.controller.api
import gitbucket.core.api.{ApiObject, ApiRef, JsonFormat}
import gitbucket.core.controller.ControllerBase
import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.ReferrerAuthenticator
import gitbucket.core.util.Implicits._
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
import scala.jdk.CollectionConverters._
import scala.util.Using
trait ApiGitReferenceControllerBase extends ControllerBase {
self: ReferrerAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase])
/*
* i. Get a reference
* https://developer.github.com/v3/git/refs/#get-a-reference
*/
get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository =>
getRef()
})
// Some versions of GHE support this path
get("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { repository =>
logger.warn("git/refs/ endpoint may not be compatible with GitHub API v3. Consider using git/ref/ endpoint instead")
getRef()
})
private def getRef() = {
val revstr = multiParams("splat").head
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val ref = git.getRepository().findRef(revstr)
if (ref != null) {
val sha = ref.getObjectId().name()
JsonFormat(ApiRef(revstr, ApiObject(sha)))
} else {
val refs = git
.getRepository()
.getRefDatabase()
.getRefsByPrefix("refs/")
.asScala
JsonFormat(refs.map { ref =>
val sha = ref.getObjectId().name()
ApiRef(revstr, ApiObject(sha))
})
}
}
}
/*
* ii. Get all references
* https://developer.github.com/v3/git/refs/#get-all-references
*/
/*
* iii. Create a reference
* https://developer.github.com/v3/git/refs/#create-a-reference
*/
/*
* iv. Update a reference
* https://developer.github.com/v3/git/refs/#update-a-reference
*/
/*
* v. Delete a reference
* https://developer.github.com/v3/git/refs/#delete-a-reference
*/
}

View File

@@ -1,79 +0,0 @@
package gitbucket.core.controller.api
import gitbucket.core.api.{ApiComment, ApiUser, CreateAComment, JsonFormat}
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName}
trait ApiIssueCommentControllerBase extends ControllerBase {
self: AccountService
with IssuesService
with RepositoryService
with HandleCommentService
with MilestonesService
with ReadableUsersAuthenticator
with ReferrerAuthenticator =>
/*
* i. List comments on an issue
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
*/
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for {
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)
})
}) getOrElse NotFound()
})
/*
* ii. List comments in a repository
* https://developer.github.com/v3/issues/comments/#list-comments-in-a-repository
*/
/*
* iii. Get a single comment
* https://developer.github.com/v3/issues/comments/#get-a-single-comment
*/
/*
* iv. Create a comment
* https://developer.github.com/v3/issues/comments/#create-a-comment
*/
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
(for {
issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString)
body <- extractFromJsonBody[CreateAComment].map(_.body) if !body.isEmpty
action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
(issue, id) <- handleComment(issue, Some(body), repository, action)
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(
ApiComment(
issueComment,
RepositoryName(repository),
issueId,
ApiUser(context.loginAccount.get),
issue.isPullRequest
)
)
}) getOrElse NotFound()
})
/*
* v. Edit a comment
* https://developer.github.com/v3/issues/comments/#edit-a-comment
*/
/*
* vi. Delete a comment
* https://developer.github.com/v3/issues/comments/#delete-a-comment
*/
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
}

View File

@@ -1,126 +0,0 @@
package gitbucket.core.controller.api
import gitbucket.core.api._
import gitbucket.core.controller.ControllerBase
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.{AccountService, IssueCreationService, IssuesService, MilestonesService}
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.PullRequestService.PullRequestLimit
import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName, UsersAuthenticator}
import gitbucket.core.util.Implicits._
trait ApiIssueControllerBase extends ControllerBase {
self: AccountService
with IssuesService
with IssueCreationService
with MilestonesService
with ReadableUsersAuthenticator
with ReferrerAuthenticator =>
/*
* i. List issues
* https://developer.github.com/v3/issues/#list-issues
* requested: 1743
*/
/*
* ii. List issues for a repository
* https://developer.github.com/v3/issues/#list-issues-for-a-repository
*/
get("/api/v3/repos/:owner/:repository/issues")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Option[Account])] =
searchIssueByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
limit = PullRequestLimit,
repos = repository.owner -> repository.name
)
JsonFormat(issues.map {
case (issue, issueUser, assignedUser) =>
ApiIssue(
issue = issue,
repositoryName = RepositoryName(repository),
user = ApiUser(issueUser),
assignee = assignedUser.map(ApiUser(_)),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository)))
)
})
})
/*
* iii. Get a single issue
* https://developer.github.com/v3/issues/#get-a-single-issue
*/
get("/api/v3/repos/:owner/:repository/issues/:id")(referrersOnly { repository =>
(for {
issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString)
users = getAccountsByUserNames(Set(issue.openedUserName) ++ issue.assignedUserName, Set())
openedUser <- users.get(issue.openedUserName)
} yield {
JsonFormat(
ApiIssue(
issue,
RepositoryName(repository),
ApiUser(openedUser),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository)))
)
)
}) getOrElse NotFound()
})
/*
* iv. Create an issue
* https://developer.github.com/v3/issues/#create-an-issue
*/
post("/api/v3/repos/:owner/:repository/issues")(readableUsersOnly { repository =>
if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator?
(for {
data <- extractFromJsonBody[CreateAnIssue]
loginAccount <- context.loginAccount
} yield {
val milestone = data.milestone.flatMap(getMilestone(repository.owner, repository.name, _))
val issue = createIssue(
repository,
data.title,
data.body,
data.assignees.headOption,
milestone.map(_.milestoneId),
None,
data.labels,
loginAccount
)
JsonFormat(
ApiIssue(
issue,
RepositoryName(repository),
ApiUser(loginAccount),
issue.assignedUserName.flatMap(getAccountByUserName(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository)))
)
)
}) getOrElse NotFound()
} else Unauthorized()
})
/*
* v. Edit an issue
* https://developer.github.com/v3/issues/#edit-an-issue
*/
/*
* vi. Lock an issue
* https://developer.github.com/v3/issues/#lock-an-issue
*/
/*
* vii. Unlock an issue
* https://developer.github.com/v3/issues/#unlock-an-issue
*/
}

View File

@@ -1,195 +0,0 @@
package gitbucket.core.controller.api
import gitbucket.core.api.{ApiError, ApiLabel, CreateALabel, JsonFormat}
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import org.scalatra.{Created, NoContent, UnprocessableEntity}
trait ApiIssueLabelControllerBase extends ControllerBase {
self: AccountService
with IssuesService
with LabelsService
with ReferrerAuthenticator
with WritableUsersAuthenticator =>
/*
* i. List all labels for this repository
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
*/
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
ApiLabel(label, RepositoryName(repository))
})
})
/*
* ii. Get a single label
* https://developer.github.com/v3/issues/labels/#get-a-single-label
*/
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
JsonFormat(ApiLabel(label, RepositoryName(repository)))
} getOrElse NotFound()
})
/*
* iii. Create a label
* https://developer.github.com/v3/issues/labels/#create-a-label
*/
post("/api/v3/repos/:owner/:repository/labels")(writableUsersOnly { repository =>
(for {
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
getLabel(repository.owner, repository.name, labelId).map { label =>
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
} getOrElse NotFound()
} 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()
})
/*
* iv. Update a label
* https://developer.github.com/v3/issues/labels/#update-a-label
*/
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository =>
(for {
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)
)
)
} 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()
})
/*
* v. Delete a label
* https://developer.github.com/v3/issues/labels/#delete-a-label
*/
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository =>
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId)
NoContent()
} getOrElse NotFound()
}
})
/*
* vi. List labels on an issue
* https://developer.github.com/v3/issues/labels/#list-labels-on-an-issue
*/
get("/api/v3/repos/:owner/:repository/issues/:id/labels")(referrersOnly { repository =>
JsonFormat(getIssueLabels(repository.owner, repository.name, params("id").toInt).map { l =>
ApiLabel(l, RepositoryName(repository.owner, repository.name))
})
})
/*
* vii. Add labels to an issue
* https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue
*/
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[Seq[String]];
issueId <- params("id").toIntOpt
} yield {
data.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel(
repository.owner,
repository.name,
createLabel(repository.owner, repository.name, labelName)
).get
)
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId, true)
ApiLabel(label, RepositoryName(repository.owner, repository.name))
}
})
})
/*
* viii. Remove a label from an issue
* https://developer.github.com/v3/issues/labels/#remove-a-label-from-an-issue
*/
delete("/api/v3/repos/:owner/:repository/issues/:id/labels/:name")(writableUsersOnly { repository =>
val issueId = params("id").toInt
val labelName = params("name")
getLabel(repository.owner, repository.name, labelName) match {
case Some(label) =>
deleteIssueLabel(repository.owner, repository.name, issueId, label.labelId, true)
JsonFormat(Seq(label))
case None =>
NotFound()
}
})
/*
* ix. Replace all labels for an issue
* https://developer.github.com/v3/issues/labels/#replace-all-labels-for-an-issue
*/
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[Seq[String]];
issueId <- params("id").toIntOpt
} yield {
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
data.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel(
repository.owner,
repository.name,
createLabel(repository.owner, repository.name, labelName)
).get
)
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId, true)
ApiLabel(label, RepositoryName(repository.owner, repository.name))
}
})
})
/*
* x. Remove all labels from an issue
* https://developer.github.com/v3/issues/labels/#remove-all-labels-from-an-issue
*/
delete("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
val issueId = params("id").toInt
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
NoContent()
})
/*
* 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

@@ -1,76 +0,0 @@
package gitbucket.core.controller.api
import gitbucket.core.api.{ApiGroup, CreateAGroup, ApiRepository, ApiUser, JsonFormat}
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{AdminAuthenticator, UsersAuthenticator}
trait ApiOrganizationControllerBase extends ControllerBase {
self: RepositoryService with AccountService with AdminAuthenticator with UsersAuthenticator =>
/*
* i. List your organizations
* https://developer.github.com/v3/orgs/#list-your-organizations
*/
get("/api/v3/user/orgs")(usersOnly {
JsonFormat(getGroupsByUserName(context.loginAccount.get.userName).flatMap(getAccountByUserName(_)).map(ApiGroup(_)))
})
/*
* ii. List all organizations
* https://developer.github.com/v3/orgs/#list-all-organizations
*/
get("/api/v3/organizations") {
JsonFormat(getAllUsers(false, true).filter(a => a.isGroupAccount).map(ApiGroup(_)))
}
/*
* iii. List user organizations
* https://developer.github.com/v3/orgs/#list-user-organizations
*/
get("/api/v3/users/:userName/orgs") {
JsonFormat(getGroupsByUserName(params("userName")).flatMap(getAccountByUserName(_)).map(ApiGroup(_)))
}
/**
* iv. Get an organization
* https://developer.github.com/v3/orgs/#get-an-organization
*/
get("/api/v3/orgs/:groupName") {
getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account =>
JsonFormat(ApiGroup(account))
} getOrElse NotFound()
}
/*
* v. Edit an organization
* https://developer.github.com/v3/orgs/#edit-an-organization
*/
/*
* ghe: i. Create an organization
* https://developer.github.com/enterprise/2.14/v3/enterprise-admin/orgs/#create-an-organization
*/
post("/api/v3/admin/organizations")(adminOnly {
for {
data <- extractFromJsonBody[CreateAGroup]
} yield {
val group = createGroup(
data.login,
data.profile_name,
data.url
)
updateGroupMembers(data.login, List(data.admin -> true))
JsonFormat(ApiGroup(group))
}
})
/*
* ghe: ii. Rename an organization
* https://developer.github.com/enterprise/2.14/v3/enterprise-admin/orgs/#rename-an-organization
*/
/*
* should implement delete an organization API?
*/
}

View File

@@ -1,256 +0,0 @@
package gitbucket.core.controller.api
import gitbucket.core.api._
import gitbucket.core.controller.ControllerBase
import gitbucket.core.model.{Account, Issue, PullRequest, Repository}
import gitbucket.core.service._
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.PullRequestService.PullRequestLimit
import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util._
import org.eclipse.jgit.api.Git
import org.scalatra.NoContent
import scala.util.Using
import scala.jdk.CollectionConverters._
trait ApiPullRequestControllerBase extends ControllerBase {
self: AccountService
with IssuesService
with PullRequestService
with RepositoryService
with MergeService
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with WritableUsersAuthenticator =>
/*
* i. Link Relations
* https://developer.github.com/v3/pulls/#link-relations
*/
/*
* ii. List pull requests
* https://developer.github.com/v3/pulls/#list-pull-requests
*/
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
searchPullRequestByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
limit = PullRequestLimit,
repos = repository.owner -> repository.name
)
JsonFormat(issues.map {
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
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))),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
})
/*
* iii. Get a single pull request
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
(for {
issueId <- params("id").toIntOpt
} yield {
JsonFormat(getApiPullRequest(repository, issueId))
}) getOrElse NotFound()
})
/*
* iv. Create a pull request
* https://developer.github.com/v3/pulls/#create-a-pull-request
* requested #1843
*/
post("/api/v3/repos/:owner/:repository/pulls")(readableUsersOnly { repository =>
(for {
data <- extractFromJsonBody[Either[CreateAPullRequest, CreateAPullRequestAlt]]
} yield {
data match {
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,
assignedUserName = None,
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
}
}
.getOrElse {
NotFound()
}
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
}
}
.getOrElse {
NotFound()
}
}
})
})
/*
* v. Update a pull request
* https://developer.github.com/v3/pulls/#update-a-pull-request
*/
/*
* vi. List commits on a pull request
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
*/
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)
}
.toList
JsonFormat(commits)
}
}
} getOrElse NotFound()
})
/*
* vii. List pull requests files
* https://developer.github.com/v3/pulls/#list-pull-requests-files
*/
/*
* viii. Get if a pull request has been merged
* https://developer.github.com/v3/pulls/#get-if-a-pull-request-has-been-merged
*/
get("/api/v3/repos/:owner/:repository/pulls/:id/merge")(referrersOnly { repository =>
(for {
issueId <- params("id").toIntOpt
(issue, pullReq) <- getPullRequest(repository.owner, repository.name, issueId)
} yield {
if (checkConflict(repository.owner, repository.name, pullReq.branch, issueId).isDefined) {
NoContent
} else {
NotFound
}
}).getOrElse(NotFound)
})
/*
* ix. Merge a pull request (Merge Button)
* https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
*/
/*
* x. Labels, assignees, and milestones
* https://developer.github.com/v3/pulls/#labels-assignees-and-milestones
*/
private def getApiPullRequest(repository: RepositoryService.RepositoryInfo, issueId: Int): Option[ApiPullRequest] = {
for {
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName),
Set.empty
)
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
}
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
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))),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
}
}
}

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