Compare commits

...

101 Commits

Author SHA1 Message Date
Naoki Takezoe
1a4961c3e1 Merge pull request #2489 from gitbucket/release-4.34.0
GitBucket 4.34.0 release
2020-07-26 02:38:51 +09:00
Naoki Takezoe
561220237f Update README and CHANGELOG 2020-07-26 02:28:47 +09:00
Naoki Takezoe
f45b85aa71 Bump gist-plugin (refs #2443) and notifications-plugin (refs #2433) 2020-07-25 21:04:45 +09:00
Naoki Takezoe
83b3a7983e Update version to 4.34.0 2020-07-25 17:52:33 +09:00
Naoki Takezoe
63d4c5054e Deny empty password (#2488) 2020-07-25 17:49:58 +09:00
Naoki Takezoe
c8f6017be9 Migrate activity log to JSON-based file from database (#2468)
This also includes:
- Record rename and transfer repository in activity log
- Check existence of repsoitory when rendering activity logs
- Drop Akka dependency
2020-07-18 23:28:40 +09:00
Naoki Takezoe
f9fcb54861 Fix coursier cache path (#2481) 2020-07-18 17:17:41 +09:00
Naoki Takezoe
3534b7172d Modify CI workflow name 2020-07-05 14:17:23 +09:00
Naoki Takezoe
42a7f974e9 Update README.md 2020-07-05 13:41:51 +09:00
Naoki Takezoe
b8e02d995a Modify artifact name 2020-07-05 13:34:44 +09:00
Naoki Takezoe
e0d038aa92 Tweak artifacts uploading (#2477) 2020-07-05 13:23:24 +09:00
Naoki Takezoe
3d01df2bdc Fix GitHuib Actions configuration 2020-07-05 12:12:17 +09:00
Naoki Takezoe
6f08f1fd23 Update README.md 2020-07-05 12:04:02 +09:00
Naoki Takezoe
3dd366b394 Replace CI badge by GitHib Actions' one 2020-07-05 12:00:59 +09:00
Naoki Takezoe
8cf4528959 Setup GitHub Actions workflow (#2476) 2020-07-05 11:30:32 +09:00
Jookia
5d77bc5d98 executable: Allow saving sessions to disk (#2474)
This commit allows saving sessions to disk in ~/.gitbucket/sessions
or wherever the gitbucket home is so that logins and other session
data persist between restarts of the program.

Usage: java -jar gitbucket.war --save_sessions
2020-07-05 11:19:21 +09:00
YHojo
572c9ef558 Add get-commits-list-api (#2470) 2020-07-04 22:08:48 +09:00
Jookia
7c736c526e theme: Add title to sidebar toggle button (#2473)
This makes it so users can mouse over the button and see what it does.
2020-07-04 22:06:54 +09:00
Naoki Takezoe
696d354f3c Repository operation restrictions (#2469) 2020-07-04 22:03:10 +09:00
Hirohisa Yamaguchi
501cbf54ab fix TLS/SSL LDAP under IBM JVM (#2472)
Co-authored-by: Hirohisa Yamaguchi <umq@ueo.co.jp>
2020-07-04 15:07:34 +09:00
kenji yoshida
c15d69d566 Update sbt-assembly to 0.15.0 2020-06-28 14:55:04 +09:00
Naoki Takezoe
83eb933230 Improve file list performance (#2466)
- Simplify an internal method to retrieving commit info
- Introduce in-memory cache for large repositories
- Introduce the upper limit for the number of files to retrieving commit info
2020-06-26 15:59:20 +09:00
kenji yoshida
43bec9b0df jgit 5.8.0.202006091008-r 2020-06-21 09:27:16 +09:00
kenji yoshida
279305c202 scalafmt 2.6.1 2020-06-20 22:55:23 +09:00
Naoki Takezoe
ecbb86c006 Support both /ref and /refs in Get Reference API (#2465) 2020-06-20 16:50:51 +09:00
kenji yoshida
921fb17ef0 scalafmt 2.6.0 2020-06-19 11:41:25 +09:00
kenji yoshida
0362de7d35 json4s 3.6.9 2020-06-16 06:34:10 +09:00
Adrian
cf0d8ea2d0 Apply the trustStore setting when ssl is true - Fixes #1782 (#2449)
Previously the truststore configuration option was applied only when StartTLS is called on a plain LDAP connection, however, the trust store is equally applicable to a full TLS connection (i.e. when ldap.ssl is set to true).
2020-06-15 01:48:03 +09:00
Naoki Takezoe
0e9026447d Fix get a reference API path (#2459) 2020-06-15 01:46:29 +09:00
kenji yoshida
cf4d9cb03c jetty 9.4.30.v20200611 2020-06-13 14:37:16 +09:00
Naoki Takezoe
2a1edeaca3 Use-defined CSS (#2458) 2020-06-08 15:50:37 +09:00
kenji yoshida
eebabf9b08 sbt 1.3.12 2020-05-31 14:08:24 +09:00
kenji yoshida
d882fcad12 Update commons-io to 2.7 2020-05-30 09:31:55 +09:00
kenji yoshida
f976290282 update testcontainers 2020-05-30 09:26:13 +09:00
kenji yoshida
f3f9d5dae2 sbt 1.3.11 2020-05-30 09:25:30 +09:00
kenji yoshida
71dffd1089 Update sbt-scalatra to 1.0.4 2020-05-22 08:54:32 +09:00
kenji yoshida
f411e98c9a Update scalafmt to 2.5.2 2020-05-22 08:51:01 +09:00
kenji yoshida
1baa489bc7 Update json4s to 3.6.8 2020-05-22 08:48:46 +09:00
kenji yoshida
f996b0fc4a update HikariCP to 3.4.5 2020-05-22 08:46:54 +09:00
kenji yoshida
eb6ba1c800 Update jetty to 9.4.29.v20200521 2020-05-22 08:45:31 +09:00
SIkebe
8fac1baa3c Fix file upload on IE11 (#2452)
Bump dropzone from 5.2.0 to 5.5.1
v5.6.0 or later version of dropzone has a compatibility problem with IE.
It breaks file upload function in issue/PR pages.
So you have to stick to v5.5.1 for now or use polyfill.
https://gitlab.com/meno/dropzone/-/issues/225
2020-05-16 23:33:49 +09:00
kenji yoshida
2d327543b9 update testcontainers 2020-05-16 08:45:40 +09:00
SIkebe
8a080efe9d Bump jQuery to 3.5.1 (#2447) 2020-05-07 09:03:05 +09:00
Naoki Takezoe
aaaf61e29e Fix browser_download_url of releases APIs (#2448) 2020-05-07 09:02:24 +09:00
kenji yoshida
1294323df5 Update testcontainers-scala to 0.37.0 2020-05-05 09:17:21 +09:00
kenji yoshida
9ef4e75746 Update scalafmt to 2.5.0 2020-05-01 22:02:37 +09:00
Pedro Rijo
ded4ab702d Implement release pagination (#2444) 2020-04-26 20:10:41 +09:00
kenji yoshida
f893d045c7 HikariCP 3.4.3 2020-04-26 13:57:12 +09:00
kenji yoshida
ef2e3adcfb Update testcontainers 2020-04-24 22:44:56 +09:00
kenji yoshida
a1908c5398 Update tika-core to 1.24.1 2020-04-22 07:44:20 +09:00
kenji yoshida
ec4f0d6531 sbt 1.3.10 2020-04-15 22:45:25 +09:00
xuwei-k
9ef366237c Update testcontainers 2020-04-14 11:45:57 +09:00
xuwei-k
9197ad2600 Update dependencies 2020-04-13 01:17:22 +09:00
Naoki Takezoe
2cb7ecd851 Drop password validation (#2439) 2020-04-11 10:58:28 +09:00
Nicolas.C
7d1ad4ce66 Delete AddPrimaryKey tag which is not required with not composed inde… (#2435)
* Delete AddPrimaryKey tag which is not required with not composed index (#2361)
* Add mariadb driver for liquibase

Co-authored-by: nico57c <>
2020-04-11 03:20:19 +09:00
SIkebe
c274acc8f4 Restrict information visibility if allowAnonymousAccess is false (#2438) 2020-04-11 02:41:05 +09:00
kenji yoshida
7a0d48dd7a sbt 1.3.9 2020-04-02 11:17:48 +09:00
kenji yoshida
9c6f9048e1 Update testcontainers-scala to 0.36.1 2020-03-16 13:07:08 +09:00
kenji yoshida
4ff4acfd7e Update mockito to 3.3.3 2020-03-14 15:52:25 +09:00
kenji yoshida
d8e9f07721 Update jgit to 5.7.0.202003110725-r 2020-03-14 15:52:02 +09:00
kenji yoshida
662c5638dd Update jgit to 5.7.0.202003090808-r 2020-03-11 07:57:39 +09:00
kenji yoshida
02b830d034 Update testcontainers-scala to 0.36.0 2020-03-09 10:25:44 +09:00
kenji yoshida
3734529e5c Update httpclient to 4.5.12 2020-03-08 09:08:37 +09:00
kenji yoshida
715ec24389 Update testcontainers 2020-03-06 07:57:13 +09:00
kenji yoshida
5615b23548 Update jetty to 9.4.27.v20200227 2020-02-29 00:42:15 +09:00
kenji yoshida
ca2eeb48cf Update mockito-core to 3.3.1 2020-02-27 11:10:59 +09:00
kenji yoshida
b063c0a80c Update thumbnailator to 0.4.11 2020-02-25 09:53:08 +09:00
kenji yoshida
ee8b5692bd Update scalafmt to 2.4.2 2020-02-23 21:53:57 +09:00
kenji yoshida
7a749dca67 Update thumbnailator to 0.4.10 2020-02-23 21:53:21 +09:00
Kasan
0378f26ee6 Ignore Limited settings, when searching repositories. (#2427) 2020-02-22 13:25:23 +09:00
kenji yoshida
4d281273c1 Update mockito-core to 3.3.0 2020-02-22 07:47:03 +09:00
kenji yoshida
e23e11e1a8 Update jgit to 5.6.1.202002131546-r 2020-02-18 07:27:26 +09:00
SIkebe
772835dd22 Fix line-through rendering in markdown (#2430) 2020-02-16 23:30:00 +09:00
kenji yoshida
bbf2e57548 Update scalafmt to 2.4.1 2020-02-16 12:34:54 +09:00
kenji yoshida
aecda130b6 Update thumbnailator to 0.4.9 2020-02-12 11:02:16 +09:00
kenji yoshida
1d48030e7c Update testcontainers-scala to 0.35.2 2020-02-10 11:54:49 +09:00
kenji yoshida
929ba2fa19 Update commons-compress to 1.20 2020-02-09 05:06:27 +09:00
kenji yoshida
6fefa947ca Update testcontainers-scala to 0.35.1 2020-02-07 20:34:19 +09:00
kenji yoshida
072cd2e846 Update sbt to 1.3.8 2020-02-04 11:50:27 +09:00
Naoki Takezoe
5d20cd0365 Remove useless configuration template 2020-01-29 23:12:04 +09:00
kenji yoshida
03ec055f66 Update mariadb-java-client to 2.5.4 2020-01-28 00:24:34 +09:00
kenji yoshida
4b6a5e5d49 Update dependencies 2020-01-24 13:12:13 +09:00
kenji yoshida
6d21e38159 Update httpclient to 4.5.11 2020-01-19 08:39:36 +09:00
kenji yoshida
a47c8249bf Update jetty to 9.4.26.v20200117 2020-01-18 16:43:20 +09:00
Kasan
59d7a672b3 Limit the visible repositories in the sidebar. (#2421) 2020-01-18 10:34:43 +09:00
SIkebe
564e95d36e Correct activity message of reopening pull request (#2420)
* Correct activity message of reopening pull request
* Fix docs
2020-01-18 10:15:54 +09:00
kenji yoshida
9aee99be74 sbt 1.3.7 2020-01-15 22:13:29 +09:00
kenji yoshida
aecd8b503d Update dependencies 2020-01-15 22:08:13 +09:00
kenji yoshida
fa65eeea35 Update scalafmt to 2.3.2 2020-01-15 22:06:48 +09:00
Naoki Takezoe
fe43584c3d Ignore metals.sbt 2020-01-15 01:42:05 +09:00
Naoki Takezoe
c255b13dfc Set scalafmt version to 2.0.1 2020-01-15 01:42:05 +09:00
Naoki Takezoe
917b204e5b Restructure settings UI (#2413) 2020-01-13 02:28:30 +09:00
SIkebe
6225fd79fc Merge pull request #2417 from gitbucket/fix-compare-view
Fix diff in compare view
2020-01-12 12:44:21 +09:00
SIkebe
cbec567ef4 Make Authorization header auth-scheme case-insencitive (#2416) 2020-01-12 10:26:04 +09:00
Naoki Takezoe
84b62242d3 Fix diff in compare view 2020-01-12 03:10:04 +09:00
kenji yoshida
f17f74c30b sbt 1.3.6 2020-01-10 12:16:01 +09:00
Naoki Takezoe
63b04d5a27 Fix docs 2020-01-03 13:17:34 +09:00
Naoki Takezoe
f02073a24c Move description of CLI options to Wiki 2020-01-03 13:04:23 +09:00
Naoki Takezoe
cb87d126de (refs #2391) Fix online editor preview 2020-01-01 23:11:22 +09:00
SIkebe
941f8002e5 Implement draft pull request related APIs (#2388) 2020-01-01 01:07:19 +09:00
Naoki Takezoe
90f4f5cd89 Fix typos in docs 2019-12-31 21:19:21 +09:00
88 changed files with 2166 additions and 1425 deletions

39
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
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: Cache
uses: actions/cache@v2
env:
cache-name: cache-sbt-libs
with:
path: |
~/.ivy2/cache
~/.sbt
~/.cache/coursier/v1
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
- 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.*

3
.gitignore vendored
View File

@@ -27,4 +27,5 @@ project/plugins/project/
# Metals specific # Metals specific
.metals .metals
.bloop .bloop
project/metals.sbt

View File

@@ -1,3 +1,4 @@
version = "1.5.1"
project.git = true project.git = true
maxColumn = 120 maxColumn = 120

View File

@@ -1,17 +0,0 @@
language: scala
sudo: true
jdk:
- openjdk8
- openjdk11
script:
- sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck 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

View File

@@ -1,12 +1,28 @@
# 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.34.0 - 26 Jul 2020
- Enhancement admin settings UI
- File upload settings
- Restrict repository operations
- User-defined CSS
- Limit the repository list in the sidebar
- Improve MariaDB support
- Improve activity logging
- CLI option to persist session on disk in the standalone mode
- Web API updates
- Add [list commits API](https://developer.github.com/v3/repos/commits/#list-commits)
- Bundled plugins updates
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) 4.18.0 -> 4.19.0
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin) 1.8.0 -> 1.9.0
### 4.33.0 - 31 Dec 2019 ### 4.33.0 - 31 Dec 2019
- All CLI options are configurable by environmental variables - All CLI options are configurable by environment variables
- Folding pull request files - Folding pull request files
- WebHook security options - WebHook security options
- Add asignee and asignees properties to some Web APIs' response - Add assignee and assignees properties to some Web APIs' response
### 4.32.0 - 7 Aug 2019 ### 4.32.0 - 7 Aug 2019

View File

@@ -1,4 +1,4 @@
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.svg)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket) [![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.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 is a Git web platform powered by Scala offering: GitBucket is a Git web platform powered by Scala offering:
@@ -31,20 +31,6 @@ 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]`
- `--upload_timeout=[MAX_UPLOAD_TIMEOUT]`
`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 in bytes for upload files *( default is 3 MB -> 3 x 1024 x 1024 )*.
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).
@@ -69,13 +55,22 @@ 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.32.x What's New in 4.33.x
------------- -------------
### 4.33.0 - 31 Dec 2019 ### 4.34.0 - 26 Jul 2020
- All CLI options are configurable by environmental variables - Enhancement admin settings UI
- Folding pull request files - File upload settings
- WebHook security options - Restrict repository operations
- Add asignee and asignees properties to some Web APIs' response - User-defined CSS
- Limit the repository list in the sidebar
- Improve MariaDB support
- Improve activity logging
- CLI option to persist session on disk in the standalone mode
- Web API updates
- Add [list commits API](https://developer.github.com/v3/repos/commits/#list-commits)
- Bundled plugins updates
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) 4.18.0 -> 4.19.0
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin) 1.8.0 -> 1.9.0
See the [change log](CHANGELOG.md) for all of the updates. See the [change log](CHANGELOG.md) for all of the updates.

View File

@@ -3,10 +3,10 @@ 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.34.0"
val ScalatraVersion = "2.7.0-RC1" val ScalatraVersion = "2.7.0-RC1"
val JettyVersion = "9.4.25.v20191220" val JettyVersion = "9.4.30.v20200611"
val JgitVersion = "5.6.0.201912101111-r" val JgitVersion = "5.8.0.202006091008-r"
lazy val root = (project in file(".")) lazy val root = (project in file("."))
.enablePlugins(SbtTwirl, ScalatraPlugin) .enablePlugins(SbtTwirl, ScalatraPlugin)
@@ -36,42 +36,40 @@ libraryDependencies ++= Seq(
"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.7", "org.json4s" %% "json4s-jackson" % "3.6.9",
"commons-io" % "commons-io" % "2.6", "commons-io" % "commons-io" % "2.7",
"io.github.gitbucket" % "solidbase" % "1.0.3", "io.github.gitbucket" % "solidbase" % "1.0.3",
"io.github.gitbucket" % "markedj" % "1.0.16", "io.github.gitbucket" % "markedj" % "1.0.16",
"org.apache.commons" % "commons-compress" % "1.19", "org.apache.commons" % "commons-compress" % "1.20",
"org.apache.commons" % "commons-email" % "1.5", "org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.6", "commons-net" % "commons-net" % "3.6",
"org.apache.httpcomponents" % "httpclient" % "4.5.10", "org.apache.httpcomponents" % "httpclient" % "4.5.12",
"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.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.23", "org.apache.tika" % "tika-core" % "1.24.1",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12", "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.199", "com.h2database" % "h2" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.5.2", "org.mariadb.jdbc" % "mariadb-java-client" % "2.6.0",
"org.postgresql" % "postgresql" % "42.2.6", "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" % "3.4.1", "com.zaxxer" % "HikariCP" % "3.4.5",
"com.typesafe" % "config" % "1.4.0", "com.typesafe" % "config" % "1.4.0",
"com.typesafe.akka" %% "akka-actor" % "2.5.27",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.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.2.4.Final", "org.cache2k" % "cache2k-all" % "1.2.4.Final",
"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.11",
"net.coobird" % "thumbnailator" % "0.4.8",
"com.github.zafarkhaja" % "java-semver" % "0.9.0", "com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "5.64.4", "com.nimbusds" % "oauth2-oidc-sdk" % "5.64.4",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test", "junit" % "junit" % "4.13" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"org.mockito" % "mockito-core" % "3.2.4" % "test", "org.mockito" % "mockito-core" % "3.3.3" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.34.2" % "test", "com.dimafeng" %% "testcontainers-scala" % "0.37.0" % "test",
"org.testcontainers" % "mysql" % "1.12.4" % "test", "org.testcontainers" % "mysql" % "1.14.3" % "test",
"org.testcontainers" % "postgresql" % "1.12.4" % "test", "org.testcontainers" % "postgresql" % "1.14.3" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0", "net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.10.1", "is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.0.3" "org.ec4j.core" % "ec4j-core" % "0.0.3"
) )

View File

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

View File

@@ -2,8 +2,8 @@ scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.0") addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.3") addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0") addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2") addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")

View File

@@ -4,6 +4,10 @@ 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;
@@ -21,6 +25,7 @@ public class JettyLauncher {
String contextPath = "/"; String contextPath = "/";
String tmpDirPath=""; String tmpDirPath="";
boolean forceHttps = false; boolean forceHttps = false;
boolean saveSessions = false;
host = getEnvironmentVariable("gitbucket.host"); host = getEnvironmentVariable("gitbucket.host");
port = getEnvironmentVariable("gitbucket.port"); port = getEnvironmentVariable("gitbucket.port");
@@ -28,6 +33,9 @@ public class JettyLauncher {
tmpDirPath = getEnvironmentVariable("gitbucket.tempDir"); 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) {
@@ -41,12 +49,6 @@ public class JettyLauncher {
case "--prefix": case "--prefix":
contextPath = dim[1]; contextPath = dim[1];
break; break;
case "--max_file_size":
System.setProperty("gitbucket.maxFileSize", dim[1]);
break;
case "--upload_timeout":
System.setProperty("gitbucket.UploadTimeout", dim[1]);
break;
case "--gitbucket.home": case "--gitbucket.home":
System.setProperty("gitbucket.home", dim[1]); System.setProperty("gitbucket.home", dim[1]);
break; break;
@@ -56,9 +58,6 @@ 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;
} }
} }
} }
@@ -96,6 +95,19 @@ 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 == null || tmpDirPath.equals("")){
tmpDir = new File(getGitBucketHome(), "tmp"); tmpDir = new File(getGitBucketHome(), "tmp");

View File

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

View File

@@ -60,13 +60,12 @@
<!-- 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"/> <column name="ACCESS_TOKEN_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_ACCESS_TOKEN_PK" primaryKey="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"/>
@@ -74,7 +73,7 @@
<!-- ACTIVITY --> <!-- ACTIVITY -->
<!--================================================================================================--> <!--================================================================================================-->
<createTable tableName="ACTIVITY"> <createTable tableName="ACTIVITY">
<column name="ACTIVITY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/> <column name="ACTIVITY_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_ACTIVITY_PK" primaryKey="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"/>
@@ -84,7 +83,6 @@
<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"/>
@@ -108,7 +106,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"/> <column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_COMMIT_COMMENT_PK" primaryKey="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"/>
@@ -119,14 +117,13 @@
<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"/> <column name="COMMIT_STATUS_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_COMMIT_STATUS_PK" primaryKey="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"/>
@@ -139,7 +136,6 @@
<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"/>
@@ -218,7 +214,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"/> <column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true" primaryKeyName="IDX_ISSUE_COMMENT_PK" primaryKey="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"/>
@@ -226,7 +222,6 @@
<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"/>
<!--================================================================================================--> <!--================================================================================================-->

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<dropTable tableName="ACTIVITY" />
</changeSet>

View File

@@ -16,7 +16,7 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
context.getSessionCookieConfig.setSecure(true) context.getSessionCookieConfig.setSecure(true)
} }
// Register TransactionFilter and BasicAuthenticationFilter at first // Register TransactionFilter at first
context.addFilter("transactionFilter", new TransactionFilter) context.addFilter("transactionFilter", new TransactionFilter)
context context
.getFilterRegistration("transactionFilter") .getFilterRegistration("transactionFilter")

View File

@@ -1,7 +1,22 @@
package gitbucket.core package gitbucket.core
import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration} import java.io.FileOutputStream
import io.github.gitbucket.solidbase.model.{Version, Module} import java.nio.charset.StandardCharsets
import java.sql.Connection
import java.util
import java.util.UUID
import gitbucket.core.model.Activity
import gitbucket.core.util.Directory.ActivityLog
import gitbucket.core.util.JDBCUtil
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration, SqlMigration}
import io.github.gitbucket.solidbase.model.{Module, Version}
import org.json4s.NoTypeHints
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
import scala.util.Using
object GitBucketCoreModule object GitBucketCoreModule
extends Module( extends Module(
@@ -65,5 +80,38 @@ object GitBucketCoreModule
new Version("4.31.1"), new Version("4.31.1"),
new Version("4.31.2"), new Version("4.31.2"),
new Version("4.32.0", new LiquibaseMigration("update/gitbucket-core_4.32.xml")), new Version("4.32.0", new LiquibaseMigration("update/gitbucket-core_4.32.xml")),
new Version("4.33.0") new Version("4.33.0"),
new Version(
"4.34.0",
new Migration() {
override def migrate(moduleId: String, version: String, context: util.Map[String, AnyRef]): Unit = {
implicit val formats = Serialization.formats(NoTypeHints)
import JDBCUtil._
val conn = context.get(Solidbase.CONNECTION).asInstanceOf[Connection]
val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") {
rs =>
Activity(
activityId = UUID.randomUUID().toString,
userName = rs.getString("USER_NAME"),
repositoryName = rs.getString("REPOSITORY_NAME"),
activityUserName = rs.getString("ACTIVITY_USER_NAME"),
activityType = rs.getString("ACTIVITY_TYPE"),
message = rs.getString("MESSAGE"),
additionalInfo = {
val additionalInfo = rs.getString("ADDITIONAL_INFO")
if (rs.wasNull()) None else Some(additionalInfo)
},
activityDate = rs.getTimestamp("ACTIVITY_DATE")
)
}
Using.resource(new FileOutputStream(ActivityLog, true)) { out =>
list.foreach { activity =>
out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8))
}
}
}
},
new LiquibaseMigration("update/gitbucket-core_4.34.xml")
)
) )

View File

@@ -21,7 +21,8 @@ case class ApiPullRequest(
body: String, body: String,
user: ApiUser, user: ApiUser,
labels: List[ApiLabel], labels: List[ApiLabel],
assignee: Option[ApiUser] assignee: Option[ApiUser],
draft: Option[Boolean]
) { ) {
val id = 0 // dummy id val id = 0 // dummy id
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}") val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
@@ -62,7 +63,8 @@ object ApiPullRequest {
body = issue.content.getOrElse(""), body = issue.content.getOrElse(""),
user = user, user = user,
labels = labels, labels = labels,
assignee = assignee assignee = assignee,
draft = Some(pullRequest.isDraft)
) )
case class Commit(sha: String, ref: String, repo: ApiRepository)(baseOwner: String) { case class Commit(sha: String, ref: String, repo: ApiRepository)(baseOwner: String) {

View File

@@ -6,7 +6,7 @@ case class ApiReleaseAsset(name: String, size: Long)(tag: String, fileName: Stri
val label = name val label = name
val file_id = fileName val file_id = fileName
val browser_download_url = ApiPath( val browser_download_url = ApiPath(
s"/api/v3/repos/${repositoryName.fullName}/releases/${tag}/assets/${fileName}" s"/${repositoryName.fullName}/releases/${tag}/assets/${fileName}"
) )
} }

View File

@@ -5,7 +5,8 @@ case class CreateAPullRequest(
head: String, head: String,
base: String, base: String,
body: Option[String], body: Option[String],
maintainer_can_modify: Option[Boolean] maintainer_can_modify: Option[Boolean],
draft: Option[Boolean]
) )
case class CreateAPullRequestAlt( case class CreateAPullRequestAlt(

View File

@@ -16,6 +16,7 @@ import gitbucket.core.util._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.BadRequest import org.scalatra.BadRequest
import org.scalatra.forms._ import org.scalatra.forms._
import org.scalatra.Forbidden
class AccountController class AccountController
extends AccountControllerBase extends AccountControllerBase
@@ -34,6 +35,7 @@ class AccountController
with WebHookService with WebHookService
with PrioritiesService with PrioritiesService
with RepositoryCreationService with RepositoryCreationService
with RequestCache
trait AccountControllerBase extends AccountManagementControllerBase { trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService self: AccountService
@@ -82,7 +84,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password", text(required, maxlength(20), password))), "password" -> trim(label("Password", text(required, maxlength(20)))),
"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( "extraMailAddresses" -> list(
@@ -94,7 +96,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)(AccountNewForm.apply) )(AccountNewForm.apply)
val editForm = mapping( val editForm = mapping(
"password" -> trim(label("Password", optional(text(maxlength(20), password)))), "password" -> trim(label("Password", optional(text(maxlength(20))))),
"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( "extraMailAddresses" -> list(
@@ -691,27 +693,28 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}") { if (context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin) {
if (getRepository(form.owner, form.name).isEmpty) { LockUtil.lock(s"${form.owner}/${form.name}") {
createRepository( if (getRepository(form.owner, form.name).isEmpty) {
context.loginAccount.get, createRepository(
form.owner, context.loginAccount.get,
form.name, form.owner,
form.description, form.name,
form.isPrivate, form.description,
form.initOption, form.isPrivate,
form.sourceUrl form.initOption,
) form.sourceUrl
)
}
} }
} // redirect to the repository
redirect(s"/${form.owner}/${form.name}")
// redirect to the repository } else Forbidden()
redirect(s"/${form.owner}/${form.name}")
}) })
get("/:owner/:repository/fork")(readableUsersOnly { repository => get("/:owner/:repository/fork")(readableUsersOnly { repository =>
if (repository.repository.options.allowFork) { val loginAccount = context.loginAccount.get
val loginAccount = context.loginAccount.get if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName) val groups = getGroupsByUserName(loginUserName)
groups match { groups match {
@@ -735,8 +738,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}) })
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
if (repository.repository.options.allowFork) { val loginAccount = context.loginAccount.get
val loginAccount = context.loginAccount.get if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val accountName = form.accountName val accountName = form.accountName
@@ -750,7 +753,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// redirect to the repository // redirect to the repository
redirect(s"/${accountName}/${repository.name}") redirect(s"/${accountName}/${repository.name}")
} }
} else BadRequest() } else Forbidden()
}) })
private def existsAccount: Constraint = new Constraint() { private def existsAccount: Constraint = new Constraint() {

View File

@@ -52,6 +52,7 @@ class ApiController
with ReferrerAuthenticator with ReferrerAuthenticator
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with WritableUsersAuthenticator with WritableUsersAuthenticator
with RequestCache
trait ApiControllerBase extends ControllerBase { trait ApiControllerBase extends ControllerBase {

View File

@@ -22,12 +22,18 @@ class DashboardController
with WebHookPullRequestReviewCommentService with WebHookPullRequestReviewCommentService
with MilestonesService with MilestonesService
with UsersAuthenticator with UsersAuthenticator
with RequestCache
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 { get("/dashboard/repos")(usersOnly {
val repos = getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true) val repos = getVisibleRepositories(
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
)
html.repos(getGroupNames(context.loginAccount.get.userName), repos, repos) html.repos(getGroupNames(context.loginAccount.get.userName), repos, repos)
}) })
@@ -93,7 +99,12 @@ trait DashboardControllerBase extends ControllerBase {
}, },
filter, filter,
getGroupNames(userName), getGroupNames(userName),
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true) getVisibleRepositories(
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
)
) )
} }
@@ -118,7 +129,12 @@ trait DashboardControllerBase extends ControllerBase {
}, },
filter, filter,
getGroupNames(userName), getGroupNames(userName),
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true) getVisibleRepositories(
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
)
) )
} }

View File

@@ -17,6 +17,7 @@ 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 scala.util.Using
import gitbucket.core.service.SystemSettingsService
/** /**
* Provides Ajax based file upload functionality. * Provides Ajax based file upload functionality.
@@ -28,11 +29,11 @@ class FileUploadController
with FileUploadSupport with FileUploadSupport
with RepositoryService with RepositoryService
with AccountService with AccountService
with ReleaseService { with ReleaseService
with SystemSettingsService {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(FileUtil.MaxFileSize)))
post("/image") { post("/image") {
setMultipartConfig()
execute( execute(
{ (file, fileId) => { (file, fileId) =>
FileUtils FileUtils
@@ -44,6 +45,7 @@ class FileUploadController
} }
post("/tmp") { post("/tmp") {
setMultipartConfig()
execute( execute(
{ (file, fileId) => { (file, fileId) =>
FileUtils FileUtils
@@ -55,6 +57,7 @@ class FileUploadController
} }
post("/file/:owner/:repository") { post("/file/:owner/:repository") {
setMultipartConfig()
execute( execute(
{ (file, fileId) => { (file, fileId) =>
FileUtils.writeByteArrayToFile( FileUtils.writeByteArrayToFile(
@@ -70,6 +73,7 @@ class FileUploadController
} }
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 =>
@@ -128,6 +132,7 @@ class FileUploadController
} }
post("/release/:owner/:repository/:tag") { post("/release/:owner/:repository/:tag") {
setMultipartConfigForLargeFile()
session session
.get(Keys.Session.LoginAccount) .get(Keys.Session.LoginAccount)
.collect { .collect {
@@ -150,6 +155,7 @@ class FileUploadController
post("/import") { post("/import") {
import JDBCUtil._ import JDBCUtil._
setMultipartConfig()
session.get(Keys.Session.LoginAccount).collect { session.get(Keys.Session.LoginAccount).collect {
case loginAccount: Account if loginAccount.isAdmin => case loginAccount: Account if loginAccount.isAdmin =>
execute({ (file, fileId) => execute({ (file, fileId) =>
@@ -159,6 +165,18 @@ class FileUploadController
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 {

View File

@@ -29,6 +29,7 @@ class IndexController
with AccessTokenService with AccessTokenService
with AccountFederationService with AccountFederationService
with OpenIDConnectService with OpenIDConnectService
with RequestCache
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService
@@ -65,7 +66,12 @@ trait IndexControllerBase extends ControllerBase {
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName) val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
gitbucket.core.html.index( gitbucket.core.html.index(
getRecentActivitiesByOwners(visibleOwnerSet), getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(Some(account), withoutPhysicalInfo = true), getVisibleRepositories(
Some(account),
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken( showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName account.userName
) )
@@ -73,7 +79,7 @@ trait IndexControllerBase extends ControllerBase {
} }
.getOrElse { .getOrElse {
gitbucket.core.html.index( gitbucket.core.html.index(
getRecentActivities(), getRecentPublicActivities(),
getVisibleRepositories(None, withoutPhysicalInfo = true), getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false showBannerToCreatePersonalAccessToken = false
) )
@@ -156,7 +162,7 @@ trait IndexControllerBase extends ControllerBase {
get("/activities.atom") { get("/activities.atom") {
contentType = "application/atom+xml; type=feed" contentType = "application/atom+xml; type=feed"
xml.feed(getRecentActivities()) xml.feed(getRecentPublicActivities())
} }
post("/sidebar-collapse") { post("/sidebar-collapse") {
@@ -279,11 +285,28 @@ trait IndexControllerBase extends ControllerBase {
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
} }
gitbucket.core.search.html.repositories(query, repositories, visibleRepositories) gitbucket.core.search.html.repositories(query, repositories, visibleRepositories)
} }
} }

View File

@@ -30,6 +30,7 @@ class IssuesController
with WebHookPullRequestReviewCommentService with WebHookPullRequestReviewCommentService
with CommitsService with CommitsService
with PrioritiesService with PrioritiesService
with RequestCache
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService self: IssuesService

View File

@@ -36,6 +36,7 @@ class PullRequestsController
with MergeService with MergeService
with ProtectedBranchService with ProtectedBranchService
with PrioritiesService with PrioritiesService
with RequestCache
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService

View File

@@ -2,7 +2,14 @@ package gitbucket.core.controller
import java.io.File import java.io.File
import gitbucket.core.service.{AccountService, ActivityService, ReleaseService, RepositoryService} import gitbucket.core.service.{
AccountService,
ActivityService,
PaginationHelper,
ReleaseService,
RepositoryService,
RequestCache
}
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
@@ -10,6 +17,7 @@ import org.scalatra.forms._
import gitbucket.core.releases.html import gitbucket.core.releases.html
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import scala.util.Using import scala.util.Using
class ReleaseController class ReleaseController
@@ -21,6 +29,7 @@ class ReleaseController
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with WritableUsersAuthenticator with WritableUsersAuthenticator
with RequestCache
trait ReleaseControllerBase extends ControllerBase { trait ReleaseControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService
@@ -42,17 +51,14 @@ trait ReleaseControllerBase extends ControllerBase {
)(ReleaseForm.apply) )(ReleaseForm.apply)
get("/:owner/:repository/releases")(referrersOnly { repository => get("/:owner/:repository/releases")(referrersOnly { repository =>
val releases = getReleases(repository.owner, repository.name) val page = PaginationHelper.page(params.get("page"))
val assets = getReleaseAssetsMap(repository.owner, repository.name)
html.list( html.list(
repository, repository,
repository.tags.reverse.map { tag => fetchReleases(repository, page),
(tag, releases.find(_.tag == tag.name).map { release => hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
(release, assets(release)) page,
}) repository.tags.size
},
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )
}) })
@@ -215,4 +221,21 @@ trait ReleaseControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/releases") 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

@@ -20,6 +20,7 @@ import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import scala.util.Using import scala.util.Using
import org.scalatra.Forbidden
class RepositorySettingsController class RepositorySettingsController
extends RepositorySettingsControllerBase extends RepositorySettingsControllerBase
@@ -29,8 +30,10 @@ class RepositorySettingsController
with ProtectedBranchService with ProtectedBranchService
with CommitStatusService with CommitStatusService
with DeployKeyService with DeployKeyService
with ActivityService
with OwnerAuthenticator with OwnerAuthenticator
with UsersAuthenticator with UsersAuthenticator
with RequestCache
trait RepositorySettingsControllerBase extends ControllerBase { trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService
@@ -39,12 +42,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
with ProtectedBranchService with ProtectedBranchService
with CommitStatusService with CommitStatusService
with DeployKeyService with DeployKeyService
with ActivityService
with OwnerAuthenticator with OwnerAuthenticator
with UsersAuthenticator => 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,
@@ -57,9 +60,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
) )
val optionsForm = mapping( val optionsForm = mapping(
"repositoryName" -> trim(
label("Repository Name", text(required, maxlength(100), repository, renameRepositoryName))
),
"description" -> trim(label("Description", optional(text()))), "description" -> trim(label("Description", optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())), "isPrivate" -> trim(label("Repository Type", boolean())),
"issuesOption" -> trim(label("Issues Option", text(required, featureOption))), "issuesOption" -> trim(label("Issues Option", text(required, featureOption))),
@@ -100,9 +100,16 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"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)
@@ -144,13 +151,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
form.mergeOptions, form.mergeOptions,
form.defaultMergeOption form.defaultMergeOption
) )
// Change repository name
if (repository.name != form.repositoryName) {
// Update database
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
}
flash.update("info", "Repository settings has been updated.") flash.update("info", "Repository settings has been updated.")
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") redirect(s"/${repository.owner}/${repository.name}/settings/options")
}) })
/** branch settings */ /** branch settings */
@@ -250,9 +252,10 @@ 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]] =
Array(h.getName, h.getValue) h.map { h =>
} Array(h.getName, h.getValue)
}
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git => git =>
@@ -364,24 +367,56 @@ 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) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
// Record activity log
recordRenameRepositoryActivity(
repository.owner,
form.repositoryName,
repository.name,
context.loginAccount.get.userName
)
}
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) =>
// Change repository owner if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) {
if (repository.owner != form.newOwner) { // Change repository owner
renameRepository(repository.owner, repository.name, form.newOwner, repository.name) if (repository.owner != form.newOwner) {
} // Update database and move git repository
redirect(s"/${form.newOwner}/${repository.name}") renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Record activity log
recordRenameRepositoryActivity(
form.newOwner,
repository.name,
repository.owner,
context.loginAccount.get.userName
)
}
redirect(s"/${form.newOwner}/${repository.name}")
} else Forbidden()
}) })
/** /**
* Delete the repository. * Delete the repository.
*/ */
post("/:owner/:repository/settings/delete")(ownerOnly { repository => post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
// Delete the repository and related files if (context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin) {
deleteRepository(repository.repository) // Delete the repository and related files
redirect(s"/${repository.owner}") deleteRepository(repository.repository)
redirect(s"/${repository.owner}")
} else Forbidden()
}) })
/** /**
@@ -418,32 +453,34 @@ 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 =
override def validate(name: String, value: String, messages: Messages): Option[String] = new Constraint() {
if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) { override def validate(name: String, value: String, messages: Messages): Option[String] =
Some(if (needExists) { if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) {
"URL had not been registered yet." Some(if (needExists) {
"URL had not been registered yet."
} else {
"URL had been registered already."
})
} else { } else {
"URL had been registered already." None
}) }
} else {
None
}
}
private def webhookEvents = new ValueType[Set[WebHook.Event]] {
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
} }
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
if (convert(name, params, messages).isEmpty) { private def webhookEvents =
Seq(name -> messages("error.required").format(name)) new ValueType[Set[WebHook.Event]] {
} else { def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
Nil WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
} }
} def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
if (convert(name, params, messages).isEmpty) {
Seq(name -> messages("error.required").format(name))
} else {
Nil
}
}
// /** // /**
// * Provides Constraint to validate the collaborator name. // * Provides Constraint to validate the collaborator name.
@@ -463,70 +500,77 @@ 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 =
override def validate( new Constraint() {
name: String, override def validate(
value: String, name: String,
params: Map[String, Seq[String]], value: String,
messages: Messages params: Map[String, Seq[String]],
): Option[String] = { messages: Messages
for { ): Option[String] = {
repoName <- params.optionValue("repository") if repoName != value for {
userName <- params.optionValue("owner") repoName <- params.optionValue("repository") if repoName != value
_ <- getRepositoryNamesOfUser(userName).find(_ == value) userName <- params.optionValue("owner")
} yield { _ <- getRepositoryNamesOfUser(userName).find(_ == value)
"Repository already exists." } yield {
"Repository already exists."
}
} }
} }
}
/** /**
*
*/ */
private def featureOption: Constraint = new Constraint() { private def featureOption: Constraint =
override def validate( new Constraint() {
name: String, override def validate(
value: String, name: String,
params: Map[String, Seq[String]], value: String,
messages: Messages params: Map[String, Seq[String]],
): Option[String] = messages: Messages
if (Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.") ): 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 =
override def validate(name: String, value: String, messages: Messages): Option[String] = new Constraint() {
getAccountByUserName(value) match { override def validate(name: String, value: String, messages: Messages): Option[String] =
case None => Some("User does not exist.") getAccountByUserName(value) match {
case Some(x) => case None => Some("User does not exist.")
if (x.userName == params("owner")) { case Some(x) =>
Some("This is current repository owner.") if (x.userName == params("owner")) {
} else { Some("This is current repository owner.")
params.get("repository").flatMap { repositoryName => } else {
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map { _ => params.get("repository").flatMap { repositoryName =>
"User already has same repository." getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map { _ =>
"User already has same repository."
}
} }
} }
} }
} }
}
private def mergeOptions = new ValueType[Seq[String]] { private def mergeOptions =
override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = { new ValueType[Seq[String]] {
params.getOrElse("mergeOptions", Nil) 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) override def validate(
if (mergeOptions.isEmpty) { name: String,
Seq("mergeOptions" -> "At least one option must be enabled.") params: Map[String, Seq[String]],
} else if (!mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))) { messages: Messages
Seq("mergeOptions" -> "mergeOptions are invalid.") ): Seq[(String, String)] = {
} else { val mergeOptions = params.getOrElse("mergeOptions", Nil)
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

@@ -60,6 +60,7 @@ class RepositoryViewerController
with WebHookPullRequestService with WebHookPullRequestService
with WebHookPullRequestReviewCommentService with WebHookPullRequestReviewCommentService
with ProtectedBranchService with ProtectedBranchService
with RequestCache
/** /**
* The repository viewer. * The repository viewer.
@@ -928,8 +929,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
val lastModifiedCommit = val lastModifiedCommit =
if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
val commitCount = JGitUtil.getCommitCount(git, lastModifiedCommit.getName)
// get files // get files
val files = JGitUtil.getFileList(git, revision, path, context.settings.baseUrl) val files = JGitUtil.getFileList(git, revision, path, context.settings.baseUrl, commitCount)
val parentPath = if (path == ".") Nil else path.split("/").toList val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown // process README.md or README.markdown
val readme = files val readme = files
@@ -950,7 +952,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repository, repository,
if (path == ".") Nil else path.split("/").toList, // current path if (path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
JGitUtil.getCommitCount(git, lastModifiedCommit.getName), commitCount,
files, files,
readme, readme,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount), hasDeveloperRole(repository.owner, repository.name, context.loginAccount),

View File

@@ -38,14 +38,22 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"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 option to create a new repository", boolean())), "isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
"repositoryOperation" -> mapping(
"create" -> trim(label("Allow all users to create repository", boolean())),
"delete" -> trim(label("Allow all users to delete repository", boolean())),
"rename" -> trim(label("Allow all users to rename repository", boolean())),
"transfer" -> trim(label("Allow all users to transfer repository", boolean())),
"fork" -> trim(label("Allow all users to fork repository", boolean()))
)(RepositoryOperation.apply),
"gravatar" -> trim(label("Gravatar", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
"ssh" -> mapping( "ssh" -> mapping(
"enabled" -> trim(label("SSH access", boolean())), "enabled" -> trim(label("SSH access", boolean())),
"host" -> trim(label("SSH host", optional(text()))), "host" -> trim(label("SSH host", optional(text()))),
"port" -> trim(label("SSH port", optional(number()))), "port" -> trim(label("SSH port", optional(number())))
)(Ssh.apply), )(Ssh.apply),
"useSMTP" -> trim(label("SMTP", boolean())), "useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked( "smtp" -> optionalIfNotChecked(
@@ -90,11 +98,18 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
)(OIDC.apply) )(OIDC.apply)
), ),
"skinName" -> trim(label("AdminLTE skin name", text(required))), "skinName" -> trim(label("AdminLTE skin name", text(required))),
"userDefinedCss" -> trim(label("User-defined CSS", optional(text()))),
"showMailAddress" -> trim(label("Show mail address", boolean())), "showMailAddress" -> trim(label("Show mail address", boolean())),
"webhook" -> mapping( "webhook" -> mapping(
"blockPrivateAddress" -> trim(label("Block private address", boolean())), "blockPrivateAddress" -> trim(label("Block private address", boolean())),
"whitelist" -> trim(label("Whitelist", multiLineText())) "whitelist" -> trim(label("Whitelist", multiLineText()))
)(WebHook.apply) )(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.enabled && settings.baseUrl.isEmpty) {
@@ -170,7 +185,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
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))), "password" -> trim(label("Password", text(required, maxlength(20)))),
"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( "extraMailAddresses" -> list(
@@ -184,7 +199,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
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)))), "password" -> trim(label("Password", optional(text(maxlength(20))))),
"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( "extraMailAddresses" -> list(
@@ -529,24 +544,26 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
} }
} }
private def members: Constraint = new Constraint() { private def members: Constraint =
override def validate(name: String, value: String, messages: Messages): Option[String] = { new Constraint() {
if (value.split(",").exists { override def validate(name: String, value: String, messages: Messages): Option[String] = {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean } if (value.split(",").exists {
}) None _.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
else Some("Must select one manager at least.") }) None
} else Some("Must select one manager at least.")
} }
}
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = { protected def disableByNotYourself(paramName: String): Constraint =
params.get(paramName).flatMap { userName => new Constraint() {
if (userName == context.loginAccount.get.userName && params.get("removed") == Some("true")) override def validate(name: String, value: String, messages: Messages): Option[String] = {
Some("You can't disable your account yourself") params.get(paramName).flatMap { userName =>
else if (userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
None Some("You can't disable your account yourself")
else
None
}
} }
} }
}
} }

View File

@@ -24,6 +24,7 @@ class WikiController
with WebHookService with WebHookService
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with RequestCache
trait WikiControllerBase extends ControllerBase { trait WikiControllerBase extends ControllerBase {
self: WikiService self: WikiService

View File

@@ -5,6 +5,7 @@ import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.ReferrerAuthenticator import gitbucket.core.util.ReferrerAuthenticator
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.Using import scala.util.Using
@@ -12,11 +13,23 @@ import scala.util.Using
trait ApiGitReferenceControllerBase extends ControllerBase { trait ApiGitReferenceControllerBase extends ControllerBase {
self: ReferrerAuthenticator => self: ReferrerAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase])
/* /*
* i. Get a reference * i. Get a reference
* https://developer.github.com/v3/git/refs/#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 => 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 val revstr = multiParams("splat").head
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val ref = git.getRepository().findRef(revstr) val ref = git.getRepository().findRef(revstr)
@@ -38,7 +51,8 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
}) })
} }
} }
}) }
/* /*
* ii. Get all references * ii. Get all references
* https://developer.github.com/v3/git/refs/#get-all-references * https://developer.github.com/v3/git/refs/#get-all-references

View File

@@ -114,7 +114,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
requestBranch = reqBranch, requestBranch = reqBranch,
commitIdFrom = commitIdFrom.getName, commitIdFrom = commitIdFrom.getName,
commitIdTo = commitIdTo.getName, commitIdTo = commitIdTo.getName,
isDraft = false, isDraft = createPullReq.draft.getOrElse(false),
loginAccount = context.loginAccount.get, loginAccount = context.loginAccount.get,
settings = context.settings settings = context.settings
) )

View File

@@ -9,6 +9,7 @@ import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName} import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import scala.jdk.CollectionConverters._
import scala.util.Using import scala.util.Using
trait ApiRepositoryCommitControllerBase extends ControllerBase { trait ApiRepositoryCommitControllerBase extends ControllerBase {
@@ -17,6 +18,33 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
* i. List commits on a repository * i. List commits on a repository
* https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
*/ */
get("/api/v3/repos/:owner/:repository/commits")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
// TODO: The following parameters need to be implemented. [:path, :author, :since, :until]
val sha = if (request.body.nonEmpty) (parse(request.body) \ "sha").extract[String] else "refs/heads/master";
Using.resource(Git.open(getRepositoryDir(owner, name))) {
git =>
val repo = git.getRepository
Using.resource(new RevWalk(repo)) {
revWalk =>
val objectId = repo.resolve(sha)
revWalk.markStart(revWalk.parseCommit(objectId))
JsonFormat(revWalk.asScala.take(30).map {
commit =>
val commitInfo = new CommitInfo(commit)
ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, commitInfo.id.toString).size
)
})
}
}
})
/* /*
* ii. Get a single commit * ii. Get a single commit

View File

@@ -1,5 +1,9 @@
package gitbucket.core.model package gitbucket.core.model
/**
* ActivityComponent has been deprecated, but keep it for binary compatibility.
*/
@deprecated("ActivityComponent has been deprecated, but keep it for binary compatibility.", "4.34.0")
trait ActivityComponent extends TemplateComponent { self: Profile => trait ActivityComponent extends TemplateComponent { self: Profile =>
import profile.api._ import profile.api._
import self._ import self._
@@ -7,14 +11,7 @@ trait ActivityComponent extends TemplateComponent { self: Profile =>
lazy val Activities = TableQuery[Activities] lazy val Activities = TableQuery[Activities]
class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
val activityId = column[Int]("ACTIVITY_ID", O AutoInc) def * = ???
val activityUserName = column[String]("ACTIVITY_USER_NAME")
val activityType = column[String]("ACTIVITY_TYPE")
val message = column[String]("MESSAGE")
val additionalInfo = column[String]("ADDITIONAL_INFO")
val activityDate = column[java.util.Date]("ACTIVITY_DATE")
def * =
(userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
} }
} }
@@ -26,5 +23,5 @@ case class Activity(
message: String, message: String,
additionalInfo: Option[String], additionalInfo: Option[String],
activityDate: java.util.Date, activityDate: java.util.Date,
activityId: Int = 0 activityId: String
) )

View File

@@ -45,7 +45,7 @@ trait CoreProfile
with Profile with Profile
with AccessTokenComponent with AccessTokenComponent
with AccountComponent with AccountComponent
with ActivityComponent with ActivityComponent // ActivityComponent has been deprecated, but keep it for binary compatibility
with CollaboratorComponent with CollaboratorComponent
with CommitCommentComponent with CommitCommentComponent
with CommitStatusComponent with CommitStatusComponent

View File

@@ -17,7 +17,9 @@ trait AccountService {
def authenticate(settings: SystemSettings, userName: String, password: String)( def authenticate(settings: SystemSettings, userName: String, password: String)(
implicit s: Session implicit s: Session
): Option[Account] = { ): Option[Account] = {
val account = if (settings.ldapAuthentication) { val account = if (password.isEmpty) {
None
} else if (settings.ldapAuthentication) {
ldapAuthentication(settings, userName, password) ldapAuthentication(settings, userName, password)
} else { } else {
defaultAuthentication(userName, password) defaultAuthentication(userName, password)

View File

@@ -3,65 +3,169 @@ package gitbucket.core.service
import gitbucket.core.model.Activity import gitbucket.core.model.Activity
import gitbucket.core.util.JGitUtil import gitbucket.core.util.JGitUtil
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.util.Directory._
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import scala.util.Using
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.util.UUID
import gitbucket.core.controller.Context
import org.apache.commons.io.input.ReversedLinesFileReader
import scala.collection.mutable.ListBuffer
trait ActivityService { trait ActivityService {
self: RequestCache =>
def deleteOldActivities(limit: Int)(implicit s: Session): Int = { private implicit val formats = Serialization.formats(NoTypeHints)
Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id =>
Activities.filter(_.activityId <= id.bind).delete private def writeLog(activity: Activity): Unit = {
} getOrElse 0 Using.resource(new FileOutputStream(ActivityLog, true)) { out =>
out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8))
}
} }
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] = def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = {
Activities if (!ActivityLog.exists()) {
.join(Repositories) List.empty
.on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) } else {
.filter { val list = new ListBuffer[Activity]
case (t1, t2) => Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
if (isPublic) { var json: String = null
(t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) while (list.length < 50 && { json = reader.readLine(); json } != null) {
} else { val activity = read[Activity](json)
(t1.activityUserName === activityUserName.bind) if (activity.activityUserName == activityUserName) {
if (isPublic == false) {
list += activity
} else {
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
list += activity
}
}
} }
}
} }
.sortBy { case (t1, t2) => t1.activityId desc } list.toList
.map { case (t1, t2) => t1 } }
.take(30) }
.list
def getRecentActivities()(implicit s: Session): List[Activity] = def getRecentPublicActivities()(implicit context: Context): List[Activity] = {
Activities if (!ActivityLog.exists()) {
.join(Repositories) List.empty
.on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) } else {
.filter { case (t1, t2) => t2.isPrivate === false.bind } val list = new ListBuffer[Activity]
.sortBy { case (t1, t2) => t1.activityId desc } Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
.map { case (t1, t2) => t1 } var json: String = null
.take(30) while (list.length < 50 && { json = reader.readLine(); json } != null) {
.list val activity = read[Activity](json)
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
list += activity
}
}
}
list.toList
}
}
def getRecentActivitiesByOwners(owners: Set[String])(implicit s: Session): List[Activity] = def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = {
Activities if (!ActivityLog.exists()) {
.join(Repositories) List.empty
.on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) } else {
.filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) } val list = new ListBuffer[Activity]
.sortBy { case (t1, t2) => t1.activityId desc } Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
.map { case (t1, t2) => t1 } var json: String = null
.take(30) while (list.length < 50 && { json = reader.readLine(); json } != null) {
.list val activity = read[Activity](json)
if (owners.contains(activity.userName)) {
list += activity
} else if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
.map(_.isPrivate)
.getOrElse(true)) {
list += activity
}
}
}
list.toList
}
}
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)( def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit = {
implicit s: Session writeLog(
): Unit = Activity(
Activities insert Activity( userName,
userName, repositoryName,
repositoryName, activityUserName,
activityUserName, "create_repository",
"create_repository", s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]", None,
None, currentDate,
currentDate UUID.randomUUID().toString
)
) )
}
def recordDeleteRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"delete_repository",
s"[user:${activityUserName}] deleted [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordTransferRepositoryActivity(
userName: String,
repositoryName: String,
oldUserName: String,
activityUserName: String
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"transfer_repository",
s"[user:${activityUserName}] transfered [repo:${oldUserName}/${repositoryName}] to [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordRenameRepositoryActivity(
userName: String,
repositoryName: String,
oldRepositoryName: String,
activityUserName: String
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"rename_repository",
s"[user:${activityUserName}] renamed [repo:${userName}/${oldRepositoryName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCreateIssueActivity( def recordCreateIssueActivity(
userName: String, userName: String,
@@ -69,16 +173,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
title: String title: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"open_issue", activityUserName,
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", "open_issue",
Some(title), s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
currentDate Some(title),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordCloseIssueActivity( def recordCloseIssueActivity(
userName: String, userName: String,
@@ -86,16 +194,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
title: String title: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"close_issue", activityUserName,
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]", "close_issue",
Some(title), s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
currentDate Some(title),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordClosePullRequestActivity( def recordClosePullRequestActivity(
userName: String, userName: String,
@@ -103,16 +215,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
title: String title: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"close_issue", activityUserName,
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]", "close_issue",
Some(title), s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
currentDate Some(title),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordReopenIssueActivity( def recordReopenIssueActivity(
userName: String, userName: String,
@@ -120,16 +236,41 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
title: String title: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"reopen_issue", activityUserName,
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]", "reopen_issue",
Some(title), s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
currentDate Some(title),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordReopenPullRequestActivity(
userName: String,
repositoryName: String,
activityUserName: String,
issueId: Int,
title: String
): Unit = {
writeLog(
Activity(
userName,
repositoryName,
activityUserName,
"reopen_issue",
s"[user:${activityUserName}] reopened pull request [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate,
UUID.randomUUID().toString
)
)
}
def recordCommentIssueActivity( def recordCommentIssueActivity(
userName: String, userName: String,
@@ -137,16 +278,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
comment: String comment: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"comment_issue", activityUserName,
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", "comment_issue",
Some(cut(comment, 200)), s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
currentDate Some(cut(comment, 200)),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordCommentPullRequestActivity( def recordCommentPullRequestActivity(
userName: String, userName: String,
@@ -154,16 +299,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
comment: String comment: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"comment_issue", activityUserName,
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]", "comment_issue",
Some(cut(comment, 200)), s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
currentDate Some(cut(comment, 200)),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordCommentCommitActivity( def recordCommentCommitActivity(
userName: String, userName: String,
@@ -171,32 +320,40 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
commitId: String, commitId: String,
comment: String comment: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"comment_commit", activityUserName,
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]", "comment_commit",
Some(cut(comment, 200)), s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
currentDate Some(cut(comment, 200)),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordCreateWikiPageActivity( def recordCreateWikiPageActivity(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
activityUserName: String, activityUserName: String,
pageName: String pageName: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"create_wiki", activityUserName,
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki", "create_wiki",
Some(pageName), s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
currentDate Some(pageName),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordEditWikiPageActivity( def recordEditWikiPageActivity(
userName: String, userName: String,
@@ -204,16 +361,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
pageName: String, pageName: String,
commitId: String commitId: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"edit_wiki", activityUserName,
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", "edit_wiki",
Some(pageName + ":" + commitId), s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
currentDate Some(pageName + ":" + commitId),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordPushActivity( def recordPushActivity(
userName: String, userName: String,
@@ -221,23 +382,27 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
branchName: String, branchName: String,
commits: List[JGitUtil.CommitInfo] commits: List[JGitUtil.CommitInfo]
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"push", activityUserName,
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", "push",
Some( s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
commits Some(
.take(5) commits
.map { commit => .take(5)
commit.id + ":" + commit.shortMessage .map { commit =>
} commit.id + ":" + commit.shortMessage
.mkString("\n") }
), .mkString("\n")
currentDate ),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordCreateTagActivity( def recordCreateTagActivity(
userName: String, userName: String,
@@ -245,16 +410,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
tagName: String, tagName: String,
commits: List[JGitUtil.CommitInfo] commits: List[JGitUtil.CommitInfo]
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"create_tag", activityUserName,
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", "create_tag",
None, s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
currentDate None,
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordDeleteTagActivity( def recordDeleteTagActivity(
userName: String, userName: String,
@@ -262,61 +431,80 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
tagName: String, tagName: String,
commits: List[JGitUtil.CommitInfo] commits: List[JGitUtil.CommitInfo]
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"delete_tag", activityUserName,
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", "delete_tag",
None, s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
currentDate None,
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordCreateBranchActivity( def recordCreateBranchActivity(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
activityUserName: String, activityUserName: String,
branchName: String branchName: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"create_branch", activityUserName,
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", "create_branch",
None, s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
currentDate None,
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordDeleteBranchActivity( def recordDeleteBranchActivity(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
activityUserName: String, activityUserName: String,
branchName: String branchName: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"delete_branch", activityUserName,
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]", "delete_branch",
None, s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
currentDate None,
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)( def recordForkActivity(
implicit s: Session userName: String,
): Unit = repositoryName: String,
Activities insert Activity( activityUserName: String,
userName, forkedUserName: String
repositoryName, ): Unit = {
activityUserName, writeLog(
"fork", Activity(
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]", userName,
None, repositoryName,
currentDate activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
None,
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordPullRequestActivity( def recordPullRequestActivity(
userName: String, userName: String,
@@ -324,16 +512,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
title: String title: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"open_pullreq", activityUserName,
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", "open_pullreq",
Some(title), s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
currentDate Some(title),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordMergeActivity( def recordMergeActivity(
userName: String, userName: String,
@@ -341,16 +533,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
issueId: Int, issueId: Int,
message: String message: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"merge_pullreq", activityUserName,
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", "merge_pullreq",
Some(message), s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
currentDate Some(message),
currentDate,
UUID.randomUUID().toString
)
) )
}
def recordReleaseActivity( def recordReleaseActivity(
userName: String, userName: String,
@@ -358,16 +554,20 @@ trait ActivityService {
activityUserName: String, activityUserName: String,
releaseName: String, releaseName: String,
tagName: String tagName: String
)(implicit s: Session): Unit = ): Unit = {
Activities insert Activity( writeLog(
userName, Activity(
repositoryName, userName,
activityUserName, repositoryName,
"release", activityUserName,
s"[user:${activityUserName}] released [release:${userName}/${repositoryName}/${tagName}:${releaseName}] at [repo:${userName}/${repositoryName}]", "release",
None, s"[user:${activityUserName}] released [release:${userName}/${repositoryName}/${tagName}:${releaseName}] at [repo:${userName}/${repositoryName}]",
currentDate None,
currentDate,
UUID.randomUUID().toString
)
) )
}
private def cut(value: String, length: Int): String = private def cut(value: String, length: Int): String =
if (value.length > length) value.substring(0, length) + "..." else value if (value.length > length) value.substring(0, length) + "..." else value

View File

@@ -39,7 +39,10 @@ trait HandleCommentService {
)) ))
case "reopen" if (issue.closed) => case "reopen" if (issue.closed) =>
false -> false ->
(Some("reopen") -> Some(recordReopenIssueActivity _)) (Some("reopen") -> Some(
if (issue.isPullRequest) recordReopenPullRequestActivity _
else recordReopenIssueActivity _
))
} }
.map { .map {
case (closed, t) => case (closed, t) =>

View File

@@ -203,7 +203,7 @@ trait IssuesService {
} }
/** /**
* Returns the search result against issues. * Returns the search result against issues.
* *
* @param condition the search condition * @param condition the search condition
* @param pullRequest if true then returns only pull requests, false then returns only issues. * @param pullRequest if true then returns only pull requests, false then returns only issues.
@@ -910,13 +910,9 @@ object IssuesService {
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
) )
def page(request: HttpServletRequest) = def page(request: HttpServletRequest) = {
try { PaginationHelper.page(param(request, "page"))
val i = param(request, "page").getOrElse("1").toInt }
if (i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
} }
case class CommitStatusInfo( case class CommitStatusInfo(

View File

@@ -0,0 +1,15 @@
package gitbucket.core.service
import scala.util.Try
object PaginationHelper {
def page(page: Option[String]) = {
page
.flatMap(pageStr => Try(pageStr.toInt).toOption)
.map(Math.max(1, _)) // remove negative pages
.getOrElse(1)
}
}

View File

@@ -1,10 +1,11 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.{Account, ReleaseTag, ReleaseAsset} import gitbucket.core.model.{Account, ReleaseAsset, ReleaseTag}
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.dateColumnType import gitbucket.core.model.Profile.dateColumnType
import gitbucket.core.util.JGitUtil
trait ReleaseService { trait ReleaseService {
self: AccountService with RepositoryService => self: AccountService with RepositoryService =>
@@ -35,10 +36,9 @@ trait ReleaseService {
ReleaseAssets.filter(x => x.byTag(owner, repository, tag)).list ReleaseAssets.filter(x => x.byTag(owner, repository, tag)).list
} }
def getReleaseAssetsMap(owner: String, repository: String)( def getReleaseAssetsMap(owner: String, repository: String, releases: Seq[ReleaseTag])(
implicit s: Session implicit s: Session
): Map[ReleaseTag, Seq[ReleaseAsset]] = { ): Map[ReleaseTag, Seq[ReleaseAsset]] = {
val releases = getReleases(owner, repository)
releases.map(rel => (rel -> getReleaseAssets(owner, repository, rel.tag))).toMap releases.map(rel => (rel -> getReleaseAssets(owner, repository, rel.tag))).toMap
} }
@@ -76,20 +76,18 @@ trait ReleaseService {
ReleaseTags.filter(x => x.byRepository(owner, repository)).sortBy(x => x.updatedDate).list ReleaseTags.filter(x => x.byRepository(owner, repository)).sortBy(x => x.updatedDate).list
} }
def getRelease(owner: String, repository: String, tag: String)(implicit s: Session): Option[ReleaseTag] = { def getReleases(owner: String, repository: String, tags: Seq[JGitUtil.TagInfo])(
//Releases filter (_.byPrimaryKey(owner, repository, releaseId)) firstOption implicit s: Session
ReleaseTags filter (_.byTag(owner, repository, tag)) firstOption ): Seq[ReleaseTag] = {
ReleaseTags
.filter(x => x.byRepository(owner, repository))
.filter(x => x.tag inSetBind tags.map(_.name))
.sortBy(x => x.updatedDate)
.list
}
def getRelease(owner: String, repository: String, tag: String)(implicit s: Session): Option[ReleaseTag] = {
ReleaseTags.filter(_.byTag(owner, repository, tag)).firstOption
} }
// def getReleaseByTag(owner: String, repository: String, tag: String)(implicit s: Session): Option[Release] = {
// Releases filter (_.byTag(owner, repository, tag)) firstOption
// }
//
// def getRelease(owner: String, repository: String, releaseId: String)(implicit s: Session): Option[Release] = {
// if (isInteger(releaseId))
// getRelease(owner, repository, releaseId.toInt)
// else None
// }
def updateRelease(owner: String, repository: String, tag: String, title: String, content: Option[String])( def updateRelease(owner: String, repository: String, tag: String, title: String, content: Option[String])(
implicit s: Session implicit s: Session
@@ -107,3 +105,9 @@ trait ReleaseService {
ReleaseTags filter (_.byPrimaryKey(owner, repository, tag)) delete ReleaseTags filter (_.byPrimaryKey(owner, repository, tag)) delete
} }
} }
object ReleaseService {
val ReleaseLimit = 10
}

View File

@@ -1,6 +1,5 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.api.JsonFormat
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
@@ -9,14 +8,11 @@ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType import gitbucket.core.model.Profile.dateColumnType
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.WebHookService.WebHookPushPayload
import gitbucket.core.util.Directory.{getRepositoryDir, getRepositoryFilesDir, getTemporaryDir, getWikiRepositoryDir} import gitbucket.core.util.Directory.{getRepositoryDir, getRepositoryFilesDir, getTemporaryDir, getWikiRepositoryDir}
import gitbucket.core.util.JGitUtil.{CommitInfo, FileInfo} import gitbucket.core.util.JGitUtil.FileInfo
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.lib.{Repository => _, _} import org.eclipse.jgit.lib.{Repository => _, _}
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import scala.util.Using import scala.util.Using
trait RepositoryService { trait RepositoryService {
@@ -119,15 +115,6 @@ trait RepositoryService {
} }
.update(newUserName, newRepositoryName) .update(newUserName, newRepositoryName)
// Updates activity fk before deleting repository because activity is sorted by activityId
// and it can't be changed by deleting-and-inserting record.
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
Activities
.filter(_.activityId === activity.activityId.bind)
.map(x => (x.userName, x.repositoryName))
.update(newUserName, newRepositoryName)
}
deleteRepositoryOnModel(oldUserName, oldRepositoryName) deleteRepositoryOnModel(oldUserName, oldRepositoryName)
RepositoryWebHooks.insertAll( RepositoryWebHooks.insertAll(
@@ -213,50 +200,6 @@ trait RepositoryService {
collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)): _* collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)): _*
) )
// Update activity messages
Activities
.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}@%")
}
.map { t =>
t.activityId -> t.message
}
.list
.foreach {
case (activityId, message) =>
Activities
.filter(_.activityId === activityId.bind)
.map(_.message)
.update(
message
.replace(
s"[repo:${oldUserName}/${oldRepositoryName}]",
s"[repo:${newUserName}/${newRepositoryName}]"
)
.replace(
s"[branch:${oldUserName}/${oldRepositoryName}#",
s"[branch:${newUserName}/${newRepositoryName}#"
)
.replace(
s"[tag:${oldUserName}/${oldRepositoryName}#",
s"[tag:${newUserName}/${newRepositoryName}#"
)
.replace(
s"[pullreq:${oldUserName}/${oldRepositoryName}#",
s"[pullreq:${newUserName}/${newRepositoryName}#"
)
.replace(
s"[issue:${oldUserName}/${oldRepositoryName}#",
s"[issue:${newUserName}/${newRepositoryName}#"
)
.replace(
s"[commit:${oldUserName}/${oldRepositoryName}@",
s"[commit:${newUserName}/${newRepositoryName}@"
)
)
}
// Move git repository // Move git repository
defining(getRepositoryDir(oldUserName, oldRepositoryName)) { dir => defining(getRepositoryDir(oldUserName, oldRepositoryName)) { dir =>
if (dir.isDirectory) { if (dir.isDirectory) {
@@ -304,7 +247,7 @@ trait RepositoryService {
} }
private def deleteRepositoryOnModel(userName: String, repositoryName: String)(implicit s: Session): Unit = { private def deleteRepositoryOnModel(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities.filter(_.byRepository(userName, repositoryName)).delete // Activities.filter(_.byRepository(userName, repositoryName)).delete
Collaborators.filter(_.byRepository(userName, repositoryName)).delete Collaborators.filter(_.byRepository(userName, repositoryName)).delete
CommitComments.filter(_.byRepository(userName, repositoryName)).delete CommitComments.filter(_.byRepository(userName, repositoryName)).delete
IssueLabels.filter(_.byRepository(userName, repositoryName)).delete IssueLabels.filter(_.byRepository(userName, repositoryName)).delete
@@ -459,6 +402,7 @@ trait RepositoryService {
/** /**
* Returns the list of visible repositories for the specified user. * Returns the list of visible repositories for the specified user.
* If repositoryUserName is given then filters results by repository owner. * If repositoryUserName is given then filters results by repository owner.
* This function is for plugin compatibility.
* *
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
@@ -470,23 +414,42 @@ trait RepositoryService {
loginAccount: Option[Account], loginAccount: Option[Account],
repositoryUserName: Option[String] = None, repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false withoutPhysicalInfo: Boolean = false
)(implicit s: Session): List[RepositoryInfo] =
getVisibleRepositories(loginAccount, repositoryUserName, withoutPhysicalInfo, false)
/**
* Returns the list of visible repositories for the specified user.
* If repositoryUserName is given then filters results by repository owner.
*
* @param loginAccount the logged in account
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @param limit if true then result will include only repositories associated with the login account.
* @return the repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(
loginAccount: Option[Account],
repositoryUserName: Option[String],
withoutPhysicalInfo: Boolean,
limit: Boolean
)(implicit s: Session): List[RepositoryInfo] = { )(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
case Some(x) if (x.isAdmin) => case Some(x) if (x.isAdmin && !limit) =>
Repositories Repositories
.join(Accounts) .join(Accounts)
.on(_.userName === _.userName) .on(_.userName === _.userName)
.filter { case (t1, t2) => t2.removed === false.bind } .filter { case (t1, t2) => t2.removed === false.bind }
.map { case (t1, t2) => t1 } .map { case (t1, t2) => t1 }
// for Normal Users // for Normal Users
case Some(x) if (!x.isAdmin) => case Some(x) if (!x.isAdmin || limit) =>
Repositories Repositories
.join(Accounts) .join(Accounts)
.on(_.userName === _.userName) .on(_.userName === _.userName)
.filter { .filter {
case (t1, t2) => case (t1, t2) =>
(t2.removed === false.bind) && ((t1.isPrivate === false.bind) || (t1.userName === x.userName) || (t2.removed === false.bind) && ((t1.isPrivate === false.bind && !limit.bind) || (t1.userName === x.userName) ||
(t1.userName in GroupMembers.filter(_.userName === x.userName.bind).map(_.groupName)) || (t1.userName in GroupMembers.filter(_.userName === x.userName.bind).map(_.groupName)) ||
(Collaborators.filter { t3 => (Collaborators.filter { t3 =>
t3.byRepository(t1.userName, t1.repositoryName) && t3.byRepository(t1.userName, t1.repositoryName) &&

View File

@@ -1,9 +1,11 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{Session, Issue, Account} import gitbucket.core.model.{Account, Issue, Repository, Session}
import gitbucket.core.util.Implicits import gitbucket.core.util.Implicits
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import Implicits.request2Session import Implicits.request2Session
import gitbucket.core.model.Profile.{Accounts, Repositories}
import gitbucket.core.model.Profile.profile.blockingApi._
/** /**
* This service is used for a view helper mainly. * This service is used for a view helper mainly.
@@ -23,21 +25,41 @@ trait RequestCache
private implicit def context2Session(implicit context: Context): Session = private implicit def context2Session(implicit context: Context): Session =
request2Session(context.request) request2Session(context.request)
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: Context): Option[Issue] = { def getIssueFromCache(userName: String, repositoryName: String, issueId: String)(
implicit context: Context
): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}") { context.cache(s"issue.${userName}/${repositoryName}#${issueId}") {
super.getIssue(userName, repositoryName, issueId) super.getIssue(userName, repositoryName, issueId)
} }
} }
def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = { def getAccountByUserNameFromCache(userName: String)(implicit context: Context): Option[Account] = {
context.cache(s"account.${userName}") { context.cache(s"account.${userName}") {
super.getAccountByUserName(userName) super.getAccountByUserName(userName)
} }
} }
def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = { def getAccountByMailAddressFromCache(mailAddress: String)(implicit context: Context): Option[Account] = {
context.cache(s"account.${mailAddress}") { context.cache(s"account.${mailAddress}") {
super.getAccountByMailAddress(mailAddress) super.getAccountByMailAddress(mailAddress)
} }
} }
def getRepositoryInfoFromCache(userName: String, repositoryName: String)(
implicit context: Context
): Option[Repository] = {
context.cache(s"repository.${userName}/${repositoryName}") {
Repositories
.join(Accounts)
.on(_.userName === _.userName)
.filter {
case (t1, t2) =>
t1.byRepository(userName, repositoryName) && t2.removed === false.bind
}
.map {
case (t1, t2) => t1
}
.firstOption
}
}
} }

View File

@@ -21,9 +21,15 @@ trait SystemSettingsService {
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString) props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString) props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(RepositoryOperationCreate, settings.repositoryOperation.create.toString)
props.setProperty(RepositoryOperationDelete, settings.repositoryOperation.delete.toString)
props.setProperty(RepositoryOperationRename, settings.repositoryOperation.rename.toString)
props.setProperty(RepositoryOperationTransfer, settings.repositoryOperation.transfer.toString)
props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString)
props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString)) settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
props.setProperty(SshEnabled, settings.ssh.enabled.toString) props.setProperty(SshEnabled, settings.ssh.enabled.toString)
settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim)) settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
@@ -69,9 +75,14 @@ trait SystemSettingsService {
} }
} }
props.setProperty(SkinName, settings.skinName.toString) props.setProperty(SkinName, settings.skinName.toString)
settings.userDefinedCss.foreach(x => props.setProperty(UserDefinedCss, x))
props.setProperty(ShowMailAddress, settings.showMailAddress.toString) props.setProperty(ShowMailAddress, settings.showMailAddress.toString)
props.setProperty(WebHookBlockPrivateAddress, settings.webHook.blockPrivateAddress.toString) props.setProperty(WebHookBlockPrivateAddress, settings.webHook.blockPrivateAddress.toString)
props.setProperty(WebHookWhitelist, settings.webHook.whitelist.mkString("\n")) props.setProperty(WebHookWhitelist, settings.webHook.whitelist.mkString("\n"))
props.setProperty(UploadMaxFileSize, settings.upload.maxFileSize.toString)
props.setProperty(UploadTimeout, settings.upload.timeout.toString)
props.setProperty(UploadLargeMaxFileSize, settings.upload.largeMaxFileSize.toString)
props.setProperty(UploadLargeTimeout, settings.upload.largeTimeout.toString)
Using.resource(new java.io.FileOutputStream(GitBucketConf)) { out => Using.resource(new java.io.FileOutputStream(GitBucketConf)) { out =>
props.store(out, null) props.store(out, null)
@@ -92,15 +103,27 @@ trait SystemSettingsService {
getValue(props, AllowAccountRegistration, false), getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true), getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true), getValue(props, IsCreateRepoOptionPublic, true),
RepositoryOperation(
create = getValue(props, RepositoryOperationCreate, true),
delete = getValue(props, RepositoryOperationDelete, true),
rename = getValue(props, RepositoryOperationRename, true),
transfer = getValue(props, RepositoryOperationTransfer, true),
fork = getValue(props, RepositoryOperationFork, true)
),
getValue(props, Gravatar, false), getValue(props, Gravatar, false),
getValue(props, Notification, false), getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None), getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, LimitVisibleRepositories, false),
Ssh( Ssh(
getValue(props, SshEnabled, false), getValue(props, SshEnabled, false),
getOptionValue[String](props, SshHost, None).map(_.trim), getOptionValue[String](props, SshHost, None).map(_.trim),
getOptionValue(props, SshPort, Some(DefaultSshPort)) getOptionValue(props, SshPort, Some(DefaultSshPort))
), ),
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP getValue(
props,
UseSMTP,
getValue(props, Notification, false)
), // handle migration scenario from only notification to useSMTP
if (getValue(props, UseSMTP, getValue(props, Notification, false))) { if (getValue(props, UseSMTP, getValue(props, Notification, false))) {
Some( Some(
Smtp( Smtp(
@@ -148,8 +171,15 @@ trait SystemSettingsService {
None None
}, },
getValue(props, SkinName, "skin-blue"), getValue(props, SkinName, "skin-blue"),
getOptionValue(props, UserDefinedCss, None),
getValue(props, ShowMailAddress, false), getValue(props, ShowMailAddress, false),
WebHook(getValue(props, WebHookBlockPrivateAddress, false), getSeqValue(props, WebHookWhitelist, "")) WebHook(getValue(props, WebHookBlockPrivateAddress, false), getSeqValue(props, WebHookWhitelist, "")),
Upload(
getValue(props, UploadMaxFileSize, 3 * 1024 * 1024),
getValue(props, UploadTimeout, 3 * 10000),
getValue(props, UploadLargeMaxFileSize, 3 * 1024 * 1024),
getValue(props, UploadLargeTimeout, 3 * 10000)
)
) )
} }
} }
@@ -167,9 +197,11 @@ object SystemSettingsService {
allowAccountRegistration: Boolean, allowAccountRegistration: Boolean,
allowAnonymousAccess: Boolean, allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean, isCreateRepoOptionPublic: Boolean,
repositoryOperation: RepositoryOperation,
gravatar: Boolean, gravatar: Boolean,
notification: Boolean, notification: Boolean,
activityLogLimit: Option[Int], activityLogLimit: Option[Int],
limitVisibleRepositories: Boolean,
ssh: Ssh, ssh: Ssh,
useSMTP: Boolean, useSMTP: Boolean,
smtp: Option[Smtp], smtp: Option[Smtp],
@@ -178,8 +210,10 @@ object SystemSettingsService {
oidcAuthentication: Boolean, oidcAuthentication: Boolean,
oidc: Option[OIDC], oidc: Option[OIDC],
skinName: String, skinName: String,
userDefinedCss: Option[String],
showMailAddress: Boolean, showMailAddress: Boolean,
webHook: WebHook webHook: WebHook,
upload: Upload
) { ) {
def baseUrl(request: HttpServletRequest): String = def baseUrl(request: HttpServletRequest): String =
@@ -198,12 +232,21 @@ object SystemSettingsService {
.fold(base)(_ + base.dropWhile(_ != ':')) .fold(base)(_ + base.dropWhile(_ != ':'))
} }
def sshAddress: Option[SshAddress] = ssh.sshHost.collect { def sshAddress: Option[SshAddress] =
case host if ssh.enabled => ssh.sshHost.collect {
SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git") case host if ssh.enabled =>
} SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git")
}
} }
case class RepositoryOperation(
create: Boolean,
delete: Boolean,
rename: Boolean,
transfer: Boolean,
fork: Boolean
)
case class Ssh( case class Ssh(
enabled: Boolean, enabled: Boolean,
sshHost: Option[String], sshHost: Option[String],
@@ -251,13 +294,15 @@ object SystemSettingsService {
host: String, host: String,
port: Int, port: Int,
user: Option[String], user: Option[String],
password: Option[String], password: Option[String]
) )
case class SshAddress(host: String, port: Int, genericUser: String) case class SshAddress(host: String, port: Int, genericUser: String)
case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String]) case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
case class Upload(maxFileSize: Long, timeout: Long, largeMaxFileSize: Long, largeTimeout: Long)
val DefaultSshPort = 29418 val DefaultSshPort = 29418
val DefaultSmtpPort = 25 val DefaultSmtpPort = 25
val DefaultLdapPort = 389 val DefaultLdapPort = 389
@@ -267,9 +312,15 @@ object SystemSettingsService {
private val AllowAccountRegistration = "allow_account_registration" private val AllowAccountRegistration = "allow_account_registration"
private val AllowAnonymousAccess = "allow_anonymous_access" private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public" private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val RepositoryOperationCreate = "repository_operation_create"
private val RepositoryOperationDelete = "repository_operation_delete"
private val RepositoryOperationRename = "repository_operation_rename"
private val RepositoryOperationTransfer = "repository_operation_transfer"
private val RepositoryOperationFork = "repository_operation_fork"
private val Gravatar = "gravatar" private val Gravatar = "gravatar"
private val Notification = "notification" private val Notification = "notification"
private val ActivityLogLimit = "activity_log_limit" private val ActivityLogLimit = "activity_log_limit"
private val LimitVisibleRepositories = "limitVisibleRepositories"
private val SshEnabled = "ssh" private val SshEnabled = "ssh"
private val SshHost = "ssh.host" private val SshHost = "ssh.host"
private val SshPort = "ssh.port" private val SshPort = "ssh.port"
@@ -301,14 +352,14 @@ object SystemSettingsService {
private val OidcClientSecret = "oidc.client_secret" private val OidcClientSecret = "oidc.client_secret"
private val OidcJwsAlgorithm = "oidc.jws_algorithm" private val OidcJwsAlgorithm = "oidc.jws_algorithm"
private val SkinName = "skinName" private val SkinName = "skinName"
private val UserDefinedCss = "userDefinedCss"
private val ShowMailAddress = "showMailAddress" private val ShowMailAddress = "showMailAddress"
private val PluginNetworkInstall = "plugin.networkInstall"
private val PluginProxyHost = "plugin.proxy.host"
private val PluginProxyPort = "plugin.proxy.port"
private val PluginProxyUser = "plugin.proxy.user"
private val PluginProxyPassword = "plugin.proxy.password"
private val WebHookBlockPrivateAddress = "webhook.block_private_address" private val WebHookBlockPrivateAddress = "webhook.block_private_address"
private val WebHookWhitelist = "webhook.whitelist" private val WebHookWhitelist = "webhook.whitelist"
private val UploadMaxFileSize = "upload.maxFileSize"
private val UploadTimeout = "upload.timeout"
private val UploadLargeMaxFileSize = "upload.largeMaxFileSize"
private val UploadLargeTimeout = "upload.largeTimeout"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = { private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
getConfigValue(key).getOrElse { getConfigValue(key).getOrElse {

View File

@@ -24,7 +24,7 @@ class ApiAuthenticationFilter extends Filter with AccessTokenService with Accoun
val response = res.asInstanceOf[HttpServletResponse] val response = res.asInstanceOf[HttpServletResponse]
Option(request.getHeader("Authorization")) Option(request.getHeader("Authorization"))
.map { .map {
case auth if auth.startsWith("token ") => case auth if auth.toLowerCase().startsWith("token ") =>
AccessTokenService.getAccountByAccessToken(auth.substring(6).trim).toRight(()) AccessTokenService.getAccountByAccessToken(auth.substring(6).trim).toRight(())
case auth if auth.startsWith("Basic ") => doBasicAuth(auth, loadSystemSettings(), request).toRight(()) case auth if auth.startsWith("Basic ") => doBasicAuth(auth, loadSystemSettings(), request).toRight(())
case _ => Left(()) case _ => Left(())

View File

@@ -240,7 +240,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
with WebHookPullRequestService with WebHookPullRequestService
with WebHookPullRequestReviewCommentService with WebHookPullRequestReviewCommentService
with CommitsService with CommitsService
with SystemSettingsService { with SystemSettingsService
with RequestCache {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil private var existIds: Seq[String] = Nil

View File

@@ -2,11 +2,10 @@ package gitbucket.core.servlet
import java.io.{File, FileOutputStream} import java.io.{File, FileOutputStream}
import akka.event.Logging
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import gitbucket.core.GitBucketCoreModule import gitbucket.core.GitBucketCoreModule
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.{ActivityService, SystemSettingsService} import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.DatabaseConfig import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.JDBCUtil._ import gitbucket.core.util.JDBCUtil._
@@ -21,8 +20,6 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
import org.apache.commons.io.{FileUtils, IOUtils} import org.apache.commons.io.{FileUtils, IOUtils}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import akka.actor.{Actor, ActorSystem, Props}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.Using import scala.util.Using
@@ -35,23 +32,23 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
private val logger = LoggerFactory.getLogger(classOf[InitializeListener]) private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
// ActorSystem for Quartz scheduler // // ActorSystem for Quartz scheduler
private val system = ActorSystem( // private val system = ActorSystem(
"job", // "job",
ConfigFactory.parseString(""" // ConfigFactory.parseString("""
|akka { // |akka {
| daemonic = on // | daemonic = on
| coordinated-shutdown.run-by-jvm-shutdown-hook = off // | coordinated-shutdown.run-by-jvm-shutdown-hook = off
| quartz { // | quartz {
| schedules { // | schedules {
| Daily { // | Daily {
| expression = "0 0 0 * * ?" // | expression = "0 0 0 * * ?"
| } // | }
| } // | }
| } // | }
|} // |}
""".stripMargin) // """.stripMargin)
) // )
override def contextInitialized(event: ServletContextEvent): Unit = { override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home") val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
@@ -95,10 +92,10 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn) PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
} }
// Start Quartz scheduler // // Start Quartz scheduler
val scheduler = QuartzSchedulerExtension(system) // val scheduler = QuartzSchedulerExtension(system)
//
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity") // scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
} }
private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = { private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
@@ -172,8 +169,8 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
} }
override def contextDestroyed(event: ServletContextEvent): Unit = { override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown Quartz scheduler // // Shutdown Quartz scheduler
system.terminate() // system.terminate()
// Shutdown plugins // Shutdown plugins
PluginRegistry.shutdown(event.getServletContext, loadSystemSettings()) PluginRegistry.shutdown(event.getServletContext, loadSystemSettings())
// Close datasource // Close datasource
@@ -181,21 +178,3 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
} }
} }
class DeleteOldActivityActor extends Actor with SystemSettingsService with ActivityService {
private val logger = Logging(context.system, this)
def receive = {
case s: String => {
loadSystemSettings().activityLogLimit.foreach { limit =>
if (limit > 0) {
Database() withTransaction { implicit session =>
val rows = deleteOldActivities(limit)
logger.info(s"Deleted ${rows} activity logs")
}
}
}
}
}
}

View File

@@ -7,7 +7,7 @@ import Directory._
import ConfigUtil._ import ConfigUtil._
import com.github.takezoe.slick.blocking.{BlockingH2Driver, BlockingJdbcProfile, BlockingMySQLDriver} import com.github.takezoe.slick.blocking.{BlockingH2Driver, BlockingJdbcProfile, BlockingMySQLDriver}
import liquibase.database.AbstractJdbcDatabase import liquibase.database.AbstractJdbcDatabase
import liquibase.database.core.{H2Database, MySQLDatabase, PostgresDatabase} import liquibase.database.core.{H2Database, MariaDBDatabase, MySQLDatabase, PostgresDatabase}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import scala.reflect.ClassTag import scala.reflect.ClassTag
@@ -78,6 +78,8 @@ object DatabaseType {
H2 H2
} else if (url.startsWith("jdbc:mysql:")) { } else if (url.startsWith("jdbc:mysql:")) {
MySQL MySQL
} else if (url.startsWith("jdbc:mariadb:")) {
MariaDb
} else if (url.startsWith("jdbc:postgresql:")) { } else if (url.startsWith("jdbc:postgresql:")) {
PostgreSQL PostgreSQL
} else { } else {
@@ -97,6 +99,12 @@ object DatabaseType {
val liquiDriver = new MySQLDatabase() val liquiDriver = new MySQLDatabase()
} }
object MariaDb extends DatabaseType {
val jdbcDriver = "org.mariadb.jdbc.Driver"
val slickDriver = BlockingMySQLDriver
val liquiDriver = new MariaDBDatabase()
}
object PostgreSQL extends DatabaseType { object PostgreSQL extends DatabaseType {
val jdbcDriver = "org.postgresql.Driver2" val jdbcDriver = "org.postgresql.Driver2"
val slickDriver = BlockingPostgresDriver val slickDriver = BlockingPostgresDriver

View File

@@ -29,6 +29,8 @@ object Directory {
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val ActivityLog = new File(GitBucketHome, "activity.log")
val RepositoryHome = s"${GitBucketHome}/repositories" val RepositoryHome = s"${GitBucketHome}/repositories"
val DatabaseHome = s"${GitBucketHome}/data" val DatabaseHome = s"${GitBucketHome}/data"
@@ -62,7 +64,7 @@ object Directory {
new File(getRepositoryFilesDir(owner, repository), "releases") new File(getRepositoryFilesDir(owner, repository), "releases")
/** /**
* Directory for files which are attached to issue. * Directory for Git LFS files.
*/ */
def getLfsDir(owner: String, repository: String): File = def getLfsDir(owner: String, repository: String): File =
new File(getRepositoryFilesDir(owner, repository), "lfs") new File(getRepositoryFilesDir(owner, repository), "lfs")

View File

@@ -92,12 +92,4 @@ object FileUtil {
name name
} }
lazy val MaxFileSize: Long = {
ConfigUtil.getConfigValue[Long]("gitbucket.maxFileSize").getOrElse(3 * 1024 * 1024)
}
lazy val UploadTimeout: Long = {
ConfigUtil.getConfigValue[Long]("gitbucket.UploadTimeout").getOrElse(3 * 10000)
}
} }

View File

@@ -290,6 +290,11 @@ object JGitUtil {
.entryCapacity(10000) .entryCapacity(10000)
.build() .build()
private val objectCommitCache = new Cache2kBuilder[ObjectId, RevCommit]() {}
.name("object-commit")
.entryCapacity(10000)
.build()
def removeCache(git: Git): Unit = { def removeCache(git: Git): Unit = {
val dir = git.getRepository.getDirectory val dir = git.getRepository.getDirectory
val keyPrefix = dir.getAbsolutePath + "@" val keyPrefix = dir.getAbsolutePath + "@"
@@ -368,9 +373,16 @@ object JGitUtil {
* @param revision the branch name or commit id * @param revision the branch name or commit id
* @param path the directory path (optional) * @param path the directory path (optional)
* @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional) * @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional)
* @return HTML of the file list * @param commitCount the number of commit of this repository (optional). If this number is greater than threshold, the commit info is cached in memory.
* @return The list of files in the specified directory. If the number of files are greater than threshold, the returned file list won't include the commit info.
*/ */
def getFileList(git: Git, revision: String, path: String = ".", baseUrl: Option[String] = None): List[FileInfo] = { def getFileList(
git: Git,
revision: String,
path: String = ".",
baseUrl: Option[String] = None,
commitCount: Int = 0
): List[FileInfo] = {
Using.resource(new RevWalk(git.getRepository)) { revWalk => Using.resource(new RevWalk(git.getRepository)) { revWalk =>
val objectId = git.getRepository.resolve(revision) val objectId = git.getRepository.resolve(revision)
if (objectId == null) return Nil if (objectId == null) return Nil
@@ -388,104 +400,85 @@ object JGitUtil {
Using.resource(treeWalk)(f) Using.resource(treeWalk)(f)
} }
} }
@tailrec @tailrec
def simplifyPath( def simplifyPath(
tuple: (ObjectId, FileMode, String, String, Option[String], RevCommit) tuple: (ObjectId, FileMode, String, String, Option[String], Option[RevCommit])
): (ObjectId, FileMode, String, String, Option[String], RevCommit) = tuple match { ): (ObjectId, FileMode, String, String, Option[String], Option[RevCommit]) =
case (oid, FileMode.TREE, name, path, _, commit) => tuple match {
(Using.resource(new TreeWalk(git.getRepository)) { walk => case (oid, FileMode.TREE, name, path, _, commit) =>
walk.addTree(oid) (Using.resource(new TreeWalk(git.getRepository)) { walk =>
// single tree child, or None walk.addTree(oid)
if (walk.next() && walk.getFileMode(0) == FileMode.TREE) { // single tree child, or None
Some( if (walk.next() && walk.getFileMode(0) == FileMode.TREE) {
( Some(
walk.getObjectId(0), (
walk.getFileMode(0), walk.getObjectId(0),
name + "/" + walk.getNameString, walk.getFileMode(0),
path + "/" + walk.getNameString, name + "/" + walk.getNameString,
None, path + "/" + walk.getNameString,
commit None,
) commit
).filterNot(_ => walk.next()) )
} else { ).filterNot(_ => walk.next())
None } else {
None
}
}) match {
case Some(child) => simplifyPath(child)
case _ => tuple
} }
}) match { case _ => tuple
case Some(child) => simplifyPath(child) }
case _ => tuple
}
case _ => tuple
}
def tupleAdd(tuple: (ObjectId, FileMode, String, String, Option[String]), rev: RevCommit) = tuple match { def appendLastCommits(
case (oid, fmode, name, path, opt) => (oid, fmode, name, path, opt, rev) fileList: List[(ObjectId, FileMode, String, String, Option[String])]
} ): List[(ObjectId, FileMode, String, String, Option[String], Option[RevCommit])] = {
fileList.map {
@tailrec case (id, mode, name, path, opt) =>
def findLastCommits( // Don't attempt to get the last commit if the number of files is very large.
result: List[(ObjectId, FileMode, String, String, Option[String], RevCommit)], if (fileList.size >= 100) {
restList: List[((ObjectId, FileMode, String, String, Option[String]), Map[RevCommit, RevCommit])], (id, mode, name, path, opt, None)
revIterator: java.util.Iterator[RevCommit] } else if (commitCount < 10000) {
): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] = { val i = git
if (restList.isEmpty) { (id, mode, name, path, opt, Some(getCommit(path)))
result } else {
} else if (!revIterator.hasNext) { // maybe, revCommit has only 1 log. other case, restList be empty val cached = objectCommitCache.getEntry(id)
result ++ restList.map { case (tuple, map) => tupleAdd(tuple, map.values.headOption.getOrElse(revCommit)) } if (cached == null) {
} else { val commit = getCommit(path)
val newCommit = revIterator.next objectCommitCache.put(id, commit)
val (thisTimeChecks, skips) = restList.partition { (id, mode, name, path, opt, Some(commit))
case (tuple, parentsMap) => parentsMap.contains(newCommit) } else {
} (id, mode, name, path, opt, Some(cached.getValue))
if (thisTimeChecks.isEmpty) {
findLastCommits(result, restList, revIterator)
} else {
var nextRest = skips
var nextResult = result
// Map[(name, oid), (tuple, parentsMap)]
val rest = scala.collection.mutable.Map(thisTimeChecks.map { t =>
(t._1._3 -> t._1._1) -> t
}: _*)
lazy val newParentsMap = newCommit.getParents.map(_ -> newCommit).toMap
useTreeWalk(newCommit) { walk =>
while (walk.next) {
rest.remove(walk.getNameString -> walk.getObjectId(0)).foreach {
case (tuple, _) =>
if (newParentsMap.isEmpty) {
nextResult +:= tupleAdd(tuple, newCommit)
} else {
nextRest +:= tuple -> newParentsMap
}
}
} }
} }
rest.values.foreach {
case (tuple, parentsMap) =>
val restParentsMap = parentsMap - newCommit
if (restParentsMap.isEmpty) {
nextResult +:= tupleAdd(tuple, parentsMap(newCommit))
} else {
nextRest +:= tuple -> restParentsMap
}
}
findLastCommits(nextResult, nextRest, revIterator)
}
} }
} }
def getCommit(path: String): RevCommit = {
git
.log()
.addPath(path)
.add(revCommit)
.setMaxCount(1)
.call()
.iterator()
.next()
}
var fileList: List[(ObjectId, FileMode, String, String, Option[String])] = Nil var fileList: List[(ObjectId, FileMode, String, String, Option[String])] = Nil
useTreeWalk(revCommit) { treeWalk => useTreeWalk(revCommit) { treeWalk =>
while (treeWalk.next()) { while (treeWalk.next()) {
val linkUrl = if (treeWalk.getFileMode(0) == FileMode.GITLINK) { val linkUrl = if (treeWalk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree, baseUrl).find(_.path == treeWalk.getPathString).map(_.viewerUrl) getSubmodules(git, revCommit.getTree, baseUrl).find(_.path == treeWalk.getPathString).map(_.viewerUrl)
} else None } else None
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, treeWalk.getPathString, linkUrl) fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(
0
), treeWalk.getNameString, treeWalk.getPathString, linkUrl)
} }
} }
revWalk.markStart(revCommit)
val it = revWalk.iterator appendLastCommits(fileList)
val lastCommit = it.next
val nextParentsMap = Option(lastCommit).map(_.getParents.map(_ -> lastCommit).toMap).getOrElse(Map())
findLastCommits(List.empty, fileList.map(a => a -> nextParentsMap), it)
.map(simplifyPath) .map(simplifyPath)
.map { .map {
case (objectId, fileMode, name, path, linkUrl, commit) => case (objectId, fileMode, name, path, linkUrl, commit) =>
@@ -494,11 +487,14 @@ object JGitUtil {
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name, name,
path, path,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage), getSummaryMessage(
commit.getName, commit.map(_.getFullMessage).getOrElse(""),
commit.getAuthorIdent.getWhen, commit.map(_.getShortMessage).getOrElse("")
commit.getAuthorIdent.getName, ),
commit.getAuthorIdent.getEmailAddress, commit.map(_.getName).getOrElse(""),
commit.map(_.getAuthorIdent.getWhen).orNull,
commit.map(_.getAuthorIdent.getName).getOrElse(""),
commit.map(_.getAuthorIdent.getEmailAddress).getOrElse(""),
linkUrl linkUrl
) )
} }
@@ -1004,11 +1000,12 @@ object JGitUtil {
*/ */
def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = { def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
@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] =
case true if (walk.getPathString == path) => Some(walk.getObjectId(0)) walk.next match {
case true => getPathObjectId(path, walk) case true if (walk.getPathString == path) => Some(walk.getObjectId(0))
case false => None case true => getPathObjectId(path, walk)
} case false => None
}
Using.resource(new TreeWalk(git.getRepository)) { treeWalk => Using.resource(new TreeWalk(git.getRepository)) { treeWalk =>
treeWalk.addTree(revTree) treeWalk.addTree(revTree)

View File

@@ -127,8 +127,14 @@ object LDAPUtil {
private def getSslProvider(): Provider = { private def getSslProvider(): Provider = {
val cachedInstance = provider.get() val cachedInstance = provider.get()
if (cachedInstance == null) { if (cachedInstance == null) {
val newInstance = Class val cls = try {
.forName("com.sun.net.ssl.internal.ssl.Provider") Class.forName("com.sun.net.ssl.internal.ssl.Provider")
} catch {
case e: ClassNotFoundException =>
Class.forName("com.ibm.jsse.IBMJSSEProvider")
case e: Throwable => throw e
}
val newInstance = cls
.getDeclaredConstructor() .getDeclaredConstructor()
.newInstance() .newInstance()
.asInstanceOf[Provider] .asInstanceOf[Provider]
@@ -149,7 +155,7 @@ object LDAPUtil {
keystore: String, keystore: String,
error: String error: String
)(f: LDAPConnection => Either[String, A]): Either[String, A] = { )(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) { if (tls || ssl) {
// Dynamically set Sun as the security provider // Dynamically set Sun as the security provider
Security.addProvider(getSslProvider()) Security.addProvider(getSslProvider())

View File

@@ -19,21 +19,6 @@ trait Validations {
} }
} }
/**
* Constraint for the password.
*/
val password: Constraint = new Constraint() {
lazy val validatePassword = ConfigUtil.getConfigValue[Boolean]("gitbucket.validate.password").getOrElse(true)
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if (validatePassword == true && !value.matches("[a-zA-Z0-9\\-_.]+")) {
Some(s"${name} contains invalid character.")
} else {
None
}
}
}
/** /**
* Constraint for the repository identifier. * Constraint for the repository identifier.
*/ */

View File

@@ -17,7 +17,7 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if (mailAddress.isEmpty) { val src = if (mailAddress.isEmpty) {
// by user name // by user name
getAccountByUserName(userName).map { account => getAccountByUserNameFromCache(userName).map { account =>
if (account.image.isEmpty && context.settings.gravatar) { if (account.image.isEmpty && context.settings.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {
@@ -28,7 +28,7 @@ trait AvatarImageProvider { self: RequestCache =>
} }
} else { } else {
// by mail address // by mail address
getAccountByMailAddress(mailAddress).map { account => getAccountByMailAddressFromCache(mailAddress).map { account =>
if (account.image.isEmpty && context.settings.gravatar) { if (account.image.isEmpty && context.settings.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {

View File

@@ -16,7 +16,7 @@ trait LinkConverter { self: RequestCache =>
val userName = repository.repository.userName val userName = repository.repository.userName
val repositoryName = repository.repository.repositoryName val repositoryName = repository.repository.repositoryName
getIssue(userName, repositoryName, issueId.toString) match { getIssueFromCache(userName, repositoryName, issueId.toString) match {
case Some(issue) => case Some(issue) =>
s"""<a href="${context.path}/${userName}/${repositoryName}/${if (issue.isPullRequest) "pull" else "issues"}/${issueId}"><strong>${StringUtil s"""<a href="${context.path}/${userName}/${repositoryName}/${if (issue.isPullRequest) "pull" else "issues"}/${issueId}"><strong>${StringUtil
.escapeHtml(title)}</strong> #${issueId}</a>""" .escapeHtml(title)}</strong> #${issueId}</a>"""
@@ -43,7 +43,7 @@ trait LinkConverter { self: RequestCache =>
escaped escaped
// convert username/project@SHA to link // convert username/project@SHA to link
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r) { m => .replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r) { m =>
getAccountByUserName(m.group(2)).map { _ => getAccountByUserNameFromCache(m.group(2)).map { _ =>
s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group( s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(
3 3
)}@${m.group(4).substring(0, 7)}</a></code>""" )}@${m.group(4).substring(0, 7)}</a></code>"""
@@ -53,7 +53,7 @@ trait LinkConverter { self: RequestCache =>
// convert username/project#Num to link // convert username/project#Num to link
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) { .replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) {
m => m =>
getIssue(m.group(2), m.group(3), m.group(4)) match { getIssueFromCache(m.group(2), m.group(3), m.group(4)) match {
case Some(issue) if (issue.isPullRequest) => case Some(issue) if (issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m.group(4)}">${m.group(2)}/${m.group( Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m.group(4)}">${m.group(2)}/${m.group(
3 3
@@ -68,7 +68,7 @@ trait LinkConverter { self: RequestCache =>
// convert username@SHA to link // convert username@SHA to link
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r) { m => .replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r) { m =>
getAccountByUserName(m.group(2)).map { _ => getAccountByUserNameFromCache(m.group(2)).map { _ =>
s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m
.group(3) .group(3)
.substring(0, 7)}</a></code>""" .substring(0, 7)}</a></code>"""
@@ -77,7 +77,7 @@ trait LinkConverter { self: RequestCache =>
// convert username#Num to link // convert username#Num to link
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) { m => .replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) { m =>
getIssue(m.group(2), repository.name, m.group(3)) match { getIssueFromCache(m.group(2), repository.name, m.group(3)) match {
case Some(issue) if (issue.isPullRequest) => case Some(issue) if (issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/pull/${m.group(3)}">${m.group(2)}#${m Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/pull/${m.group(3)}">${m.group(2)}#${m
.group(3)}</a>""") .group(3)}</a>""")
@@ -92,7 +92,7 @@ trait LinkConverter { self: RequestCache =>
// convert issue id to link // convert issue id to link
.replaceBy(("(?<=(^|\\W))(GH-|(?<!&)" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r) { m => .replaceBy(("(?<=(^|\\W))(GH-|(?<!&)" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r) { m =>
val prefix = if (m.group(2) == "issue:") "#" else m.group(2) val prefix = if (m.group(2) == "issue:") "#" else m.group(2)
getIssue(repository.owner, repository.name, m.group(3)) match { getIssueFromCache(repository.owner, repository.name, m.group(3)) match {
case Some(issue) if (issue.isPullRequest) => case Some(issue) if (issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m
.group(3)}</a>""") .group(3)}</a>""")
@@ -106,7 +106,7 @@ trait LinkConverter { self: RequestCache =>
// convert @username to link // convert @username to link
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r) { m => .replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r) { m =>
getAccountByUserName(m.group(2)).map { _ => getAccountByUserNameFromCache(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>""" s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
} }
} }

View File

@@ -3,7 +3,6 @@ package gitbucket.core.view
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.{Date, Locale, TimeZone} import java.util.{Date, Locale, TimeZone}
import com.nimbusds.jose.util.JSONObjectUtils
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.CommitState import gitbucket.core.model.CommitState
import gitbucket.core.model.PullRequest import gitbucket.core.model.PullRequest
@@ -196,7 +195,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
import scala.util.matching.Regex._ import scala.util.matching.Regex._
implicit class RegexReplaceString(private val s: String) extends AnyVal { implicit class RegexReplaceString(private val s: String) extends AnyVal {
def replaceAll(pattern: String, replacer: (Match) => String): String = { def replaceAll(pattern: String)(replacer: Match => String): String = {
pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$")) pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$"))
} }
} }
@@ -204,50 +203,64 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/** /**
* Convert link notations in the activity message. * Convert link notations in the activity message.
*/ */
// format: off
def activityMessage(message: String)(implicit context: Context): Html = def activityMessage(message: String)(implicit context: Context): Html =
Html( Html(
message message
.replaceAll( .replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
"\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]", if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""" s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
) } else {
.replaceAll( s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
"\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]", }
s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""" }
) .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""") if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
.replaceAll( s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
"\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", } else {
(m: Match) => s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil }
.escapeHtml( }
m.group(3) .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m =>
)}</a>""" if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
) s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}">${m.group(1)}/${m.group(2)}</a>"""
.replaceAll( } else {
"\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"${m.group(1)}/${m.group(2)}"
(m: Match) => }
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil }
.escapeHtml( .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
m.group(3) if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
)}</a>""" s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>"""
) } else {
.replaceAll("\\[user:([^\\s]+?)\\]", (m: Match) => user(m.group(1)).body) StringUtil.escapeHtml(m.group(3))
.replaceAll( }
"\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", }
(m: Match) => .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
.group(2)}@${m.group(3).substring(0, 7)}</a>""" s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>"""
) } else {
.replaceAll( StringUtil.escapeHtml(m.group(3))
"\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]", }
(m: Match) => }
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil .replaceAll("\\[user:([^\\s]+?)\\]") { m =>
.escapeHtml( user(m.group(1)).body
m.group(4) }
)}</a>""" .replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]") { m =>
) if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>"""
} else {
s"${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}"
}
}
.replaceAll("\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]") { m =>
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(4))}</a>"""
} else {
StringUtil.escapeHtml(m.group(4))
}
}
) )
// format: off
/** /**
* Remove html tags from the given Html instance. * Remove html tags from the given Html instance.
@@ -333,9 +346,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
content: Html content: Html
)(implicit context: Context): Html = )(implicit context: Context): Html =
(if (mailAddress.isEmpty) { (if (mailAddress.isEmpty) {
getAccountByUserName(userName) getAccountByUserNameFromCache(userName)
} else { } else {
getAccountByMailAddress(mailAddress) getAccountByMailAddressFromCache(mailAddress)
}).map { account => }).map { account =>
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""") Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""")
} getOrElse content } getOrElse content
@@ -494,4 +507,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
case class CommentDiffLine(newLine: Option[String], oldLine: Option[String], `type`: String, text: String) case class CommentDiffLine(newLine: Option[String], oldLine: Option[String], `type`: String, text: String)
def appendQueryString(baseUrl: String, queryString: String): String = {
s"$baseUrl${if (baseUrl.contains("?")) "&" else "?"}$queryString"
}
} }

View File

@@ -7,12 +7,16 @@
<form action="@context.path/admin/system" method="POST" validate="true" class="form-horizontal" autocomplete="off"> <form action="@context.path/admin/system" method="POST" validate="true" class="form-horizontal" autocomplete="off">
<ul class="nav nav-tabs fill-width" id="pullreq-tab"> <ul class="nav nav-tabs fill-width" id="pullreq-tab">
<li><a href="#system">System settings</a></li> <li><a href="#system">System settings</a></li>
<li><a href="#integrations">Integrations</a></li>
<li><a href="#authentication">Authentication</a></li> <li><a href="#authentication">Authentication</a></li>
</ul> </ul>
<div class="tab-content fill-width" style="padding-top: 20px;"> <div class="tab-content fill-width" style="padding-top: 20px;">
<div class="tab-pane" id="system"> <div class="tab-pane" id="system">
@settings_system(info) @settings_system(info)
</div> </div>
<div class="tab-pane" id="integrations">
@settings_integrations(info)
</div>
<div class="tab-pane" id="authentication"> <div class="tab-pane" id="authentication">
@settings_authentication(info) @settings_authentication(info)
</div> </div>
@@ -30,6 +34,9 @@ $(function(){
if(location.hash == '#authentication'){ if(location.hash == '#authentication'){
$('li:has(a[href="#authentication"])').addClass('active'); $('li:has(a[href="#authentication"])').addClass('active');
$('div#authentication').addClass('active'); $('div#authentication').addClass('active');
} else if(location.hash == '#integrations'){
$('li:has(a[href="#integrations"])').addClass('active');
$('div#integrations').addClass('active');
} else { } else {
$('li:has(a[href="#system"])').addClass('active'); $('li:has(a[href="#system"])').addClass('active');
$('div#system').addClass('active'); $('div#system').addClass('active');

View File

@@ -0,0 +1,196 @@
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.util.DatabaseConfig
<!--====================================================================-->
<!-- Services -->
<!--====================================================================-->
<label class="strong">Services</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
Use Gravatar for profile images
</label>
</fieldset>
<!--====================================================================-->
<!-- SSH access -->
<!--====================================================================-->
<hr>
<label class="strong">SSH access</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
Enable SSH access to git repository
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
</label>
</fieldset>
<div class="ssh">
<div class="form-group">
<label class="control-label col-md-2" for="sshHost">SSH host</label>
<div class="col-md-10">
<input type="text" id="sshHost" name="ssh.host" class="form-control" value="@context.settings.ssh.sshHost"/>
<span id="error-ssh_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshPort">SSH port</label>
<div class="col-md-10">
<input type="text" id="sshPort" name="ssh.port" class="form-control" value="@context.settings.ssh.sshPort"/>
<span id="error-ssh_port" class="error"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Communication email -->
<!--====================================================================-->
<hr>
<label class="strong">Communication</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="useSMTP" name="useSMTP" @if(context.settings.useSMTP){ checked}/>
SMTP
<span class="muted normal">(Enable notification as well as SMTP configuration if you want to send notification email too)</span>
</label>
</fieldset>
<div class="useSMTP">
<div class="form-group">
<label class="control-label col-md-2" for="smtpHost">SMTP host</label>
<div class="col-md-10">
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpPort">SMTP port</label>
<div class="col-md-10">
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpUser">SMTP user</label>
<div class="col-md-10">
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpPassword">SMTP password</label>
<div class="col-md-10">
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpSsl">Enable SSL</label>
<div class="col-md-10">
<input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpStarttls">Enable STARTTLS</label>
<div class="col-md-10">
<input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="fromAddress">FROM address</label>
<div class="col-md-10">
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="fromName">FROM name</label>
<div class="col-md-10">
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/>
</div>
</div>
<div class="text-right">
Send test mail to:
<input type="text" id="testAddress" size="30"/>
<input type="button" id="sendTestMail" value="Send"/>
</div>
</div>
<!--====================================================================-->
<!-- Notification email -->
<!--====================================================================-->
<hr>
<label class="strong">Notifications</label>
<fieldset>
<label class="checkbox" for="notification">
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
Send notifications
</label>
</fieldset>
<!--====================================================================-->
<!-- Web hook -->
<!--====================================================================-->
<hr>
<label class="strong">Web hook</label>
<fieldset>
<label class="checkbox" for="blockPrivateAddress">
<input type="checkbox" id="blockPrivateAddress" name="webhook.blockPrivateAddress"@if(context.settings.webHook.blockPrivateAddress){ checked}/>
Block sending to private addresses
</label>
</fieldset>
<div class="webhook">
<label><span class="strong">IP whitelist</span></label>
<fieldset>
<textarea name="webhook.whitelist" class="form-control" style="height: 100px;">@context.settings.webHook.whitelist.mkString("\n")</textarea>
</fieldset>
</div>
<script>
$(function(){
$('#sendTestMail').click(function(){
var host = $('#smtpHost' ).val();
var port = $('#smtpPort' ).val();
var user = $('#smtpUser' ).val();
var password = $('#smtpPassword').val();
var ssl = $('#smtpSsl' ).prop('checked');
var starttls = $('#smtpStarttls').prop('checked');
var fromAddress = $('#fromAddress' ).val();
var fromName = $('#fromName' ).val();
var testAddress = $('#testAddress' ).val();
if(host == ''){
alert('SMTP Host is required.');
$('#smtpHost').focus();
} else if(testAddress == ''){
alert('Destination is required.');
$('#testAddress').focus();
} else {
$.post('@context.path/admin/system/sendmail', {
'smtp.host': host,
'smtp.port': port,
'smtp.user': user,
'smtp.password': password,
'smtp.ssl': ssl,
'smtp.starttls': starttls,
'smtp.fromAddress': fromAddress,
'smtp.fromName': fromName,
'testAddress': testAddress
}, function(data, status){
if(data != ''){
alert(data);
}
});
}
});
$('#sshEnabled').change(function(){
$('.ssh input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#useSMTP').change(function(){
$('.useSMTP input').prop('disabled', !$(this).prop('checked'));
// With only SMTP in current version, notification cannot be enabled if no communication channel exists
$('#notification').prop('disabled', !$(this).prop('checked'));
if (!$(this).prop('checked')) {
// With only SMTP in current version, if SMTP is unchecked then we disable notification
$('#notification').prop('checked', false);
}
}).change();
$('#blockPrivateAddress').change(function(){
$('.webhook textarea').prop('disabled', !$(this).prop('checked'));
}).change();
});
</script>

View File

@@ -94,6 +94,14 @@
</div> </div>
</div> </div>
<!--====================================================================--> <!--====================================================================-->
<!-- User-defined CSS -->
<!--====================================================================-->
<hr>
<label><span class="strong">User-defined CSS</span></label>
<fieldset>
<textarea name="userDefinedCss" class="form-control" style="height: 100px;">@context.settings.userDefinedCss</textarea>
</fieldset>
<!--====================================================================-->
<!-- Account registration --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
@@ -108,8 +116,80 @@
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span> <span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
</label> </label>
</fieldset> </fieldset>
<!--====================================================================-->
<!-- Repository operations -->
<!--====================================================================-->
<hr> <hr>
<label class="strong">Default permissions when creating a new repository</label> <label class="strong">Repository operation</label>
<fieldset>
<div class="form-group">
<label class="control-label col-md-2">Create</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.create" value="true"@if(context.settings.repositoryOperation.create){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can create repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.create" value="false"@if(!context.settings.repositoryOperation.create){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Delete</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.delete" value="true"@if(context.settings.repositoryOperation.delete){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can delete repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.delete" value="false"@if(!context.settings.repositoryOperation.delete){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Rename</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.rename" value="true"@if(context.settings.repositoryOperation.rename){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can rename repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.rename" value="false"@if(!context.settings.repositoryOperation.rename){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Transfer</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="true"@if(context.settings.repositoryOperation.transfer){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can transfer repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="false"@if(!context.settings.repositoryOperation.transfer){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span>
</label>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Fork</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.fork" value="true"@if(context.settings.repositoryOperation.fork){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can fork repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.fork" value="false"@if(!context.settings.repositoryOperation.fork){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span>
</label>
</div>
</div>
</fieldset>
<hr>
<label class="strong">Default visibility when creating a new repository</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}> <input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
@@ -151,6 +231,41 @@
</label> </label>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- File upload -->
<!--====================================================================-->
<hr>
<label class="strong">File upload</label>
<fieldset>
<div class="form-group">
<label class="control-label col-md-2" for="uploadMaxFileSize">Max size</label>
<div class="col-md-10">
<input type="text" id="uploadMaxFileSize" name="upload.maxFileSize" class="form-control input-mini" value="@context.settings.upload.maxFileSize"/>
<span id="error-upload_maxFileSize" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="uploadTimeout">Timeout</label>
<div class="col-md-10">
<input type="text" id="uploadTimeout" name="upload.timeout" class="form-control input-mini" value="@context.settings.upload.timeout"/>
<span id="error-upload_timeout" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="uploadLargeMaxFileSize">Max size for large files</label>
<div class="col-md-10">
<input type="text" id="uploadLargeMaxFileSize" name="upload.largeMaxFileSize" class="form-control input-mini" value="@context.settings.upload.largeMaxFileSize"/>
<span id="error-upload_largeMaxFileSize" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="uploadLargeTimeout">Timeout for large files</label>
<div class="col-md-10">
<input type="text" id="uploadLargeTimeout" name="upload.largeTimeout" class="form-control input-mini" value="@context.settings.upload.largeTimeout"/>
<span id="error-upload_largeTimeout" class="error"></span>
</div>
</div>
</fieldset>
<!--====================================================================-->
<!-- Activity --> <!-- Activity -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
@@ -165,141 +280,20 @@
</div> </div>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Services --> <!-- Sidebar -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label class="strong">Services</label> <label><span class="strong">Show Repositories in Sidebar</span></label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="radio">
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/> <input type="radio" name="limitVisibleRepositories" value="false"@if(!context.settings.limitVisibleRepositories){ checked}>
Use Gravatar for profile images <span class="strong">All</span> <span class="normal">- Show all repositories in sidebar.</span>
</label>
<label class="radio">
<input type="radio" name="limitVisibleRepositories" value="true"@if(context.settings.limitVisibleRepositories){ checked}>
<span class="strong">Limited</span> <span class="normal">- Limit the visible repositories in sidebar.</span>
</label> </label>
</fieldset> </fieldset>
<!--====================================================================-->
<!-- SSH access -->
<!--====================================================================-->
<hr>
<label class="strong">SSH access</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
Enable SSH access to git repository
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
</label>
</fieldset>
<div class="ssh">
<div class="form-group">
<label class="control-label col-md-2" for="sshHost">SSH host</label>
<div class="col-md-10">
<input type="text" id="sshHost" name="ssh.host" class="form-control" value="@context.settings.ssh.sshHost"/>
<span id="error-ssh_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshPort">SSH port</label>
<div class="col-md-10">
<input type="text" id="sshPort" name="ssh.port" class="form-control" value="@context.settings.ssh.sshPort"/>
<span id="error-ssh_port" class="error"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Communication email -->
<!--====================================================================-->
<hr>
<label class="strong">Communication</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="useSMTP" name="useSMTP" @if(context.settings.useSMTP){ checked}/>
SMTP
<span class="muted normal">(Enable notification as well as SMTP configuration if you want to send notification email too)</span>
</label>
</fieldset>
<div class="useSMTP">
<div class="form-group">
<label class="control-label col-md-2" for="smtpHost">SMTP host</label>
<div class="col-md-10">
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpPort">SMTP port</label>
<div class="col-md-10">
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpUser">SMTP user</label>
<div class="col-md-10">
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpPassword">SMTP password</label>
<div class="col-md-10">
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpSsl">Enable SSL</label>
<div class="col-md-10">
<input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="smtpStarttls">Enable STARTTLS</label>
<div class="col-md-10">
<input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="fromAddress">FROM address</label>
<div class="col-md-10">
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="fromName">FROM name</label>
<div class="col-md-10">
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/>
</div>
</div>
<div class="text-right">
Send test mail to:
<input type="text" id="testAddress" size="30"/>
<input type="button" id="sendTestMail" value="Send"/>
</div>
</div>
<!--====================================================================-->
<!-- Notification email -->
<!--====================================================================-->
<hr>
<label class="strong">Notifications</label>
<fieldset>
<label class="checkbox" for="notification">
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
Send notifications
</label>
</fieldset>
<!--====================================================================-->
<!-- Web hook -->
<!--====================================================================-->
<hr>
<label class="strong">Web hook</label>
<fieldset>
<label class="checkbox" for="blockPrivateAddress">
<input type="checkbox" id="blockPrivateAddress" name="webhook.blockPrivateAddress"@if(context.settings.webHook.blockPrivateAddress){ checked}/>
Block sending to private addresses
</label>
</fieldset>
<div class="webhook">
<label><span class="strong">IP whitelist</span></label>
<fieldset>
<textarea name="webhook.whitelist" class="form-control" style="height: 100px;">@context.settings.webHook.whitelist.mkString("\n")</textarea>
</fieldset>
</div>
<script> <script>
$(function(){ $(function(){
$('#skinName').change(function(evt) { $('#skinName').change(function(evt) {
@@ -309,61 +303,5 @@ $(function(){
themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val())); themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val()));
$(document.body).removeClass(oldVal).addClass(that.val()); $(document.body).removeClass(oldVal).addClass(that.val());
}); });
$('#sendTestMail').click(function(){
var host = $('#smtpHost' ).val();
var port = $('#smtpPort' ).val();
var user = $('#smtpUser' ).val();
var password = $('#smtpPassword').val();
var ssl = $('#smtpSsl' ).prop('checked');
var starttls = $('#smtpStarttls').prop('checked');
var fromAddress = $('#fromAddress' ).val();
var fromName = $('#fromName' ).val();
var testAddress = $('#testAddress' ).val();
if(host == ''){
alert('SMTP Host is required.');
$('#smtpHost').focus();
} else if(testAddress == ''){
alert('Destination is required.');
$('#testAddress').focus();
} else {
$.post('@context.path/admin/system/sendmail', {
'smtp.host': host,
'smtp.port': port,
'smtp.user': user,
'smtp.password': password,
'smtp.ssl': ssl,
'smtp.starttls': starttls,
'smtp.fromAddress': fromAddress,
'smtp.fromName': fromName,
'testAddress': testAddress
}, function(data, status){
if(data != ''){
alert(data);
}
});
}
});
$('#sshEnabled').change(function(){
$('.ssh input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#useSMTP').change(function(){
$('.useSMTP input').prop('disabled', !$(this).prop('checked'));
// With only SMTP in current version, notification cannot be enabled if no communication channel exists
$('#notification').prop('disabled', !$(this).prop('checked'));
if (!$(this).prop('checked')) {
// With only SMTP in current version, if SMTP is unchecked then we disable notification
$('#notification').prop('checked', false);
}
}).change();
$('#blockPrivateAddress').change(function(){
$('.webhook textarea').prop('disabled', !$(this).prop('checked'));
}).change();
}); });
</script> </script>

View File

@@ -7,21 +7,24 @@
@activities.map { activity => @activities.map { activity =>
<div class="block"> <div class="block">
@(activity.activityType match { @(activity.activityType match {
case "open_issue" => detailActivity(activity, "issue-opened") case "open_issue" => simpleActivity(activity)
case "comment_issue" => detailActivity(activity, "comment-discussion") case "comment_issue" => simpleActivity(activity)
case "comment_commit" => detailActivity(activity, "comment-discussion") case "comment_commit" => simpleActivity(activity)
case "close_issue" => detailActivity(activity, "issue-closed") case "close_issue" => simpleActivity(activity)
case "reopen_issue" => detailActivity(activity, "issue-reopened") case "reopen_issue" => simpleActivity(activity)
case "open_pullreq" => detailActivity(activity, "git-pull-request") case "open_pullreq" => simpleActivity(activity)
case "merge_pullreq" => detailActivity(activity, "git-merge") case "merge_pullreq" => simpleActivity(activity)
case "release" => detailActivity(activity, "package") case "release" => simpleActivity(activity)
case "create_repository" => simpleActivity(activity, "repo") case "create_repository" => simpleActivity(activity)
case "create_branch" => simpleActivity(activity, "git-branch") case "delete_repository" => simpleActivity(activity)
case "delete_branch" => simpleActivity(activity, "circle-slash") case "rename_repository" => simpleActivity(activity)
case "create_tag" => simpleActivity(activity, "tag") case "transfer_repository" => simpleActivity(activity)
case "delete_tag" => simpleActivity(activity, "circle-slash") case "create_branch" => simpleActivity(activity)
case "fork" => simpleActivity(activity, "repo-forked") case "delete_branch" => simpleActivity(activity)
case "push" => customActivity(activity, "git-commit"){ case "create_tag" => simpleActivity(activity)
case "delete_tag" => simpleActivity(activity)
case "fork" => simpleActivity(activity)
case "push" => customActivity(activity){
<div class="small activity-message"> <div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){ if(i == 3){
@@ -37,12 +40,12 @@
}} }}
</div> </div>
} }
case "create_wiki" => customActivity(activity, "book"){ case "create_wiki" => customActivity(activity){
<div class="small activity-message"> <div class="small activity-message">
Created <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>. Created <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div> </div>
} }
case "edit_wiki" => customActivity(activity, "book"){ case "edit_wiki" => customActivity(activity){
activity.additionalInfo.get.split(":") match { activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) => case Array(pageName, commitId) =>
<div class="small activity-message"> <div class="small activity-message">
@@ -60,26 +63,7 @@
} }
} }
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = { @customActivity(activity: gitbucket.core.model.Activity)(additionalInfo: Any) = {
@*
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
*@
<div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
<div class=" activity-message">@additionalInfo</div>
}
</div>
}
@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = {
@*
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
*@
<div> <div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div> <div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong"> <div class="strong">
@@ -90,10 +74,7 @@
</div> </div>
} }
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = { @simpleActivity(activity: gitbucket.core.model.Activity) = {
@*
<div class="activity-icon-small"><i class="octicon octicon-@image"></i></div>
*@
<div> <div>
<span class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</span> <span class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</span>
<div> <div>

View File

@@ -65,9 +65,8 @@ $(function(){
} }
@dropzone(clickable: Boolean, textareaId: Option[String]) = { @dropzone(clickable: Boolean, textareaId: Option[String]) = {
url: '@context.path/upload/file/@repository.owner/@repository.name', url: '@context.path/upload/file/@repository.owner/@repository.name',
maxFilesize: @{FileUtil.MaxFileSize / 1024 / 1024}, maxFilesize: @{context.settings.upload.maxFileSize / 1024 / 1024},
//timeout defaults to 30 secs timeout: @{context.settings.upload.timeout},
timeout: @{FileUtil.UploadTimeout},
clickable: @clickable, clickable: @clickable,
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>", previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) { success: function(file, id) {

View File

@@ -1,10 +1,12 @@
@(latestUpdatedDate: java.util.Date, @(latestUpdatedDate: java.util.Date,
recentOnly: Boolean = true) recentOnly: Boolean = true)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
<span data-toggle="tooltip" title="@helpers.datetime(latestUpdatedDate)"> @if(latestUpdatedDate != null){
@if(recentOnly){ <span data-toggle="tooltip" title="@helpers.datetime(latestUpdatedDate)">
@helpers.datetimeAgoRecentOnly(latestUpdatedDate) @if(recentOnly){
} else { @helpers.datetimeAgoRecentOnly(latestUpdatedDate)
@helpers.datetimeAgo(latestUpdatedDate) } else {
} @helpers.datetimeAgo(latestUpdatedDate)
</span> }
</span>
}

View File

@@ -104,8 +104,8 @@
} else { } else {
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i" class="diffText"></div> <div id="diffText-@i" class="diffText"></div>
<input type="hidden" id="newText-@i" data-file-name="@diff.oldPath" value="@diff.newContent"> <input type="hidden" id="newText-@i" data-file-name="@diff.oldPath" data-val="@diff.newContent">
<input type="hidden" id="oldText-@i" data-file-name="@diff.newPath" value="@diff.oldContent"> <input type="hidden" id="oldText-@i" data-file-name="@diff.newPath" data-val="@diff.oldContent">
} else { } else {
@if(diff.newIsImage || diff.oldIsImage){ @if(diff.newIsImage || diff.oldIsImage){
<div class="diff-image-render diff2up"> <div class="diff-image-render diff2up">

View File

@@ -1,4 +1,5 @@
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String) @import gitbucket.core.view.helpers
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String = "")
@defining(gitbucket.core.view.Pagination(page, count, limit, width)){ p => @defining(gitbucket.core.view.Pagination(page, count, limit, width)){ p =>
@if(p.count > p.limit){ @if(p.count > p.limit){
<div> <div>
@@ -6,7 +7,7 @@
@if(page == 1){ @if(page == 1){
<li class="disabled"><span>&#9664;</span></li> <li class="disabled"><span>&#9664;</span></li>
} else { } else {
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li> <li><a href="@helpers.appendQueryString(baseURL, s"page=${page - 1 }")">&#9664;</a></li>
} }
@for(i <- 1 to p.max){ @for(i <- 1 to p.max){
@if(i == p.max && p.omitRight){ @if(i == p.max && p.omitRight){
@@ -16,7 +17,7 @@
<li class="active"><span>@i</span></li> <li class="active"><span>@i</span></li>
} else { } else {
@if(p.visibleFor(i)){ @if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li> <li><a href="@helpers.appendQueryString(baseURL, s"page=$i")">@i</a></li>
} }
} }
@if(i == 1 && p.omitLeft){ @if(i == 1 && p.omitLeft){
@@ -26,7 +27,7 @@
@if(page == p.max){ @if(page == p.max){
<li class="disabled"><span>&#9654;</span></li> <li class="disabled"><span>&#9654;</span></li>
} else { } else {
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li> <li><a href="@helpers.appendQueryString(baseURL, s"page=${page + 1}")">&#9654;</a></li>
} }
</ul> </ul>
</div> </div>

View File

@@ -11,7 +11,9 @@
repository: gitbucket.core.service.RepositoryService.RepositoryInfo, repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
isEditable: Boolean, isEditable: Boolean,
isManageable: Boolean)(implicit context: gitbucket.core.controller.Context) isManageable: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@gitbucket.core.html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){ @gitbucket.core.html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu(target, repository){ @gitbucket.core.html.menu(target, repository){
<ul class="nav nav-pills pull-left" style="line-height: 14px; margin-bottom: 10px;"> <ul class="nav nav-pills pull-left" style="line-height: 14px; margin-bottom: 10px;">

View File

@@ -36,7 +36,7 @@
<link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.structure.min.css")" rel="stylesheet"> <link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.structure.min.css")" rel="stylesheet">
<link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.theme.min.css")" rel="stylesheet"> <link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.theme.min.css")" rel="stylesheet">
<link href="@helpers.assets("/common/css/gitbucket.css")" rel="stylesheet"> <link href="@helpers.assets("/common/css/gitbucket.css")" rel="stylesheet">
<script src="@helpers.assets("/vendors/jquery/jquery-3.4.1.min.js")"></script> <script src="@helpers.assets("/vendors/jquery/jquery-3.5.1.min.js")"></script>
<script src="@helpers.assets("/vendors/jquery-ui/jquery-ui.min.js")"></script> <script src="@helpers.assets("/vendors/jquery-ui/jquery-ui.min.js")"></script>
<script src="@helpers.assets("/vendors/dropzone/dropzone.min.js")"></script> <script src="@helpers.assets("/vendors/dropzone/dropzone.min.js")"></script>
<script src="@helpers.assets("/common/js/validation.js")"></script> <script src="@helpers.assets("/common/js/validation.js")"></script>
@@ -55,6 +55,9 @@
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" /> <meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
} }
<script src="@helpers.assets("/vendors/AdminLTE-2.4.2/js/adminlte.min.js")" type="text/javascript"></script> <script src="@helpers.assets("/vendors/AdminLTE-2.4.2/js/adminlte.min.js")" type="text/javascript"></script>
@context.settings.userDefinedCss.map { css =>
<style type="text/css">@css</style>
}
</head> </head>
<body class="@context.settings.skinName page-load @if(body.toString.contains("menu-item-hover")){sidebar-mini} @if(context.sidebarCollapse){sidebar-collapse}"> <body class="@context.settings.skinName page-load @if(body.toString.contains("menu-item-hover")){sidebar-mini} @if(context.sidebarCollapse){sidebar-collapse}">
<div class="wrapper"> <div class="wrapper">
@@ -69,7 +72,7 @@
<nav class="navbar navbar-static-top" role="navigation"> <nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button--> <!-- Sidebar toggle button-->
@if(body.toString.contains("main-sidebar")){ @if(body.toString.contains("main-sidebar")){
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button"> <a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button" title="Toggle navigation">
<span class="sr-only">Toggle navigation</span> <span class="sr-only">Toggle navigation</span>
</a> </a>
} }
@@ -99,7 +102,9 @@
<ul class="dropdown-menu pull-right" style="width: auto;"> <ul class="dropdown-menu pull-right" style="width: auto;">
<li> <li>
<ul class="menu"> <ul class="menu">
<li><a href="@context.path/new">New repository</a></li> @if(context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin){
<li><a href="@context.path/new">New repository</a></li>
}
<li><a href="@context.path/groups/new">New group</a></li> <li><a href="@context.path/groups/new">New group</a></li>
</ul> </ul>
</li> </li>

View File

@@ -75,7 +75,7 @@
@gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent => @gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent =>
@repositoryHeaderComponent(repository, context) @repositoryHeaderComponent(repository, context)
} }
@if(repository.repository.options.allowFork) { @if(repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
@if(context.loginAccount.isEmpty){ @if(context.loginAccount.isEmpty){
<a class="btn btn-default btn-sm" href="@context.path/signin?redirect=@helpers.urlEncode(s"${context.path}/${repository.owner}/${repository.name}")"> <a class="btn btn-default btn-sm" href="@context.path/signin?redirect=@helpers.urlEncode(s"${context.path}/${repository.owner}/${repository.name}")">
<span class="strong"><i class="octicon octicon-repo-forked"></i>Fork</span><span class="muted">: @repository.forkedCount</span> <span class="strong"><i class="octicon octicon-repo-forked"></i>Fork</span><span class="muted">: @repository.forkedCount</span>

View File

@@ -71,10 +71,9 @@ $(function(){
}); });
$("#drop").dropzone({ $("#drop").dropzone({
maxFilesize: @{gitbucket.core.util.FileUtil.MaxFileSize / 1024 / 1024},
url: '@context.path/upload/release/@repository.owner/@repository.name/@helpers.encodeRefName(tag.name)', url: '@context.path/upload/release/@repository.owner/@repository.name/@helpers.encodeRefName(tag.name)',
//timeout defaults to 30 secs maxFilesize: @{context.settings.upload.largeMaxFileSize / 1024 / 1024},
timeout: @{gitbucket.core.util.FileUtil.UploadTimeout}, timeout: @{context.settings.upload.largeTimeout},
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>", previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) { success: function(file, id) {
var attach = var attach =

View File

@@ -1,6 +1,8 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, @(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
releases: Seq[(gitbucket.core.util.JGitUtil.TagInfo, Option[(gitbucket.core.model.ReleaseTag, Seq[gitbucket.core.model.ReleaseAsset])])], releases: Seq[(gitbucket.core.util.JGitUtil.TagInfo, Option[(gitbucket.core.model.ReleaseTag, Seq[gitbucket.core.model.ReleaseAsset])])],
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) hasWritePermission: Boolean,
page: Int, totalReleases: Int)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@gitbucket.core.html.main("Releases" + s" - ${repository.owner}/${repository.name}", Some(repository)){ @gitbucket.core.html.main("Releases" + s" - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("releases", repository){ @gitbucket.core.html.menu("releases", repository){
@@ -61,5 +63,9 @@
} }
</tbody> </tbody>
</table> </table>
<div class="pull-right">
@gitbucket.core.helper.html.paginator(page, totalReleases, gitbucket.core.service.ReleaseService.ReleaseLimit, 10)
</div>
} }
} }

View File

@@ -138,7 +138,6 @@ $(function(){
$('#btn-code').click(function(){ $('#btn-code').click(function(){
$('#editor').show(); $('#editor').show();
$('#preview').hide(); $('#preview').hide();
$('#btn-code').appendClass('active');
$('#btn-preview').removeClass('active'); $('#btn-preview').removeClass('active');
}); });
@@ -146,7 +145,6 @@ $(function(){
$('#editor').hide(); $('#editor').hide();
$('#preview').show(); $('#preview').show();
$('#btn-code').removeClass('active'); $('#btn-code').removeClass('active');
$('#btn-preview').addClass('active');
@if(fileName.map(helpers.isRenderable _).getOrElse(false)) { @if(fileName.map(helpers.isRenderable _).getOrElse(false)) {
// update preview // update preview

View File

@@ -51,11 +51,12 @@
$(function(){ $(function(){
$('#upload-area').dropzone({ $('#upload-area').dropzone({
url: '@context.path/upload/tmp', url: '@context.path/upload/tmp',
maxFilesize: @{FileUtil.MaxFileSize / 1024 / 1024}, maxFilesize: @{context.settings.upload.maxFileSize / 1024 / 1024},
timeout: @{context.settings.upload.timeout},
clickable: true, clickable: true,
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>", previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) { success: function(file, id) {
file.previewElement.remove(); $(file.previewElement).remove();
$('#upload-files').append($('<li class="upload-file">') $('#upload-files').append($('<li class="upload-file">')
.append($('<span>').data('id', id).text(file.name)) .append($('<span>').data('id', id).text(file.name))
.append($('<a class="delete" href="javascript:void(0);" style="margin-left: 4px;">(delete)</a>'))); .append($('<a class="delete" href="javascript:void(0);" style="margin-left: 4px;">(delete)</a>')));

View File

@@ -7,32 +7,8 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Danger Zone</div> <div class="panel-heading strong">Danger Zone</div>
<div class="panel-body"> <div class="panel-body">
<form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off">
<fieldset class="form-group">
<label class="strong">Transfer Ownership</label>
<div>
Transfer this repo to another user or to group.
<div class="pull-right">
@gitbucket.core.helper.html.account("newOwner", 200, true, true)
<input type="submit" class="btn btn-danger" value="Transfer"/>
<div>
<span id="error-newOwner" class="error"></span>
</div>
</div>
</div>
</fieldset>
</form>
<form id="delete-form" method="post" action="@helpers.url(repository)/settings/delete">
<fieldset class="border-top form-group">
<label class="strong">Delete repository</label>
<div>
Once you delete a repository, there is no going back.
<input type="submit" class="btn btn-danger pull-right" value="Delete this repository"/>
</div>
</fieldset>
</form>
<form id="gc-form" method="post" action="@helpers.url(repository)/settings/gc"> <form id="gc-form" method="post" action="@helpers.url(repository)/settings/gc">
<fieldset class="border-top form-group"> <fieldset class="form-group">
<label class="strong">Garbage collection</label> <label class="strong">Garbage collection</label>
<div> <div>
Run garbage collection for this git repository immediately. Run garbage collection for this git repository immediately.
@@ -40,6 +16,51 @@
</div> </div>
</fieldset> </fieldset>
</form> </form>
@if(context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin){
<form id="rename-form" method="post" action="@helpers.url(repository)/settings/rename" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Rename repository</label>
<div>
Rename this repository. The current URL will be unavailable.
<div class="pull-right">
<input type="text" name="repositoryName" id="repositoryName" class="form-control" style="width: 200px; display: inline; vertical-align: middle;" value="@repository.name"/>
<input type="submit" class="btn btn-danger" value="Rename"/>
<div>
<span id="error-repositoryName" class="error"></span>
</div>
</div>
</div>
</fieldset>
</form>
}
@if(context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
<form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Transfer Ownership</label>
<div>
Transfer this repo to another user or to group.
<div class="pull-right">
@gitbucket.core.helper.html.account("newOwner", 200, true, true)
<input type="submit" class="btn btn-danger" value="Transfer"/>
<div>
<span id="error-newOwner" class="error"></span>
</div>
</div>
</div>
</fieldset>
</form>
}
@if(context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin){
<form id="delete-form" method="post" action="@helpers.url(repository)/settings/delete">
<fieldset class="border-top form-group">
<label class="strong">Delete repository</label>
<div>
Once you delete a repository, there is no going back.
<input type="submit" class="btn btn-danger pull-right" value="Delete this repository"/>
</div>
</fieldset>
</form>
}
</div> </div>
</div> </div>
} }
@@ -50,6 +71,13 @@ $(function(){
$('#delete-form').submit(function(){ $('#delete-form').submit(function(){
return confirm('Once you delete a repository, there is no going back.\nAre you sure?'); return confirm('Once you delete a repository, there is no going back.\nAre you sure?');
}); });
$('#rename-form').submit(function(){
if($('#rename-form').data('validated') === true){
return confirm('Rename this repository as you entered. The current URL will be unavailable.\nAre you sure?');
} else {
return true;
}
});
$('#transfer-form').submit(function(){ $('#transfer-form').submit(function(){
if($('#transfer-form').data('validated') === true){ if($('#transfer-form').data('validated') === true){
return confirm('Transfer to the repository owner you entered.\nAre you sure?'); return confirm('Transfer to the repository owner you entered.\nAre you sure?');

View File

@@ -9,11 +9,6 @@
<div class="panel-heading strong">Settings</div> <div class="panel-heading strong">Settings</div>
<div class="panel-body"> <div class="panel-body">
<fieldset class="form-group"> <fieldset class="form-group">
<label for="repositoryName" class="strong">Repository Name:</label>
<input type="text" name="repositoryName" id="repositoryName" class="form-control" value="@repository.name"/>
<span id="error-repositoryName" class="error"></span>
</fieldset>
<fieldset class="border-top form-group">
<label for="description" class="strong">Description:</label> <label for="description" class="strong">Description:</label>
<input type="text" name="description" id="description" class="form-control" value="@repository.repository.description"/> <input type="text" name="description" id="description" class="form-control" value="@repository.repository.description"/>
</fieldset> </fieldset>

View File

@@ -5,11 +5,13 @@
<div class="content-wrapper main-center"> <div class="content-wrapper main-center">
<div class="content body"> <div class="content body">
<div class="signin-form"> <div class="signin-form">
@context.settings.information.map { information => @if(context.settings.allowAnonymousAccess){
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;"> @context.settings.information.map { information =>
<button type="button" class="close" data-dismiss="alert">&times;</button> <div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
@Html(information) <button type="button" class="close" data-dismiss="alert">&times;</button>
</div> @Html(information)
</div>
}
} }
@gitbucket.core.helper.html.error(error) @gitbucket.core.helper.html.error(error)
@gitbucket.core.html.signinform(context.settings, userName, password) @gitbucket.core.html.signinform(context.settings, userName, password)

View File

@@ -48,7 +48,8 @@ $(function(){
try { try {
$('#content1').dropzone({ $('#content1').dropzone({
url: '@context.path/upload/wiki/@repository.owner/@repository.name', url: '@context.path/upload/wiki/@repository.owner/@repository.name',
maxFilesize: @{FileUtil.MaxFileSize / 1024 / 1024}, maxFilesize: @{context.settings.upload.maxFileSize / 1024 / 1024},
timeout: @{context.settings.upload.timeout},
clickable: false, clickable: false,
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>", previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) { success: function(file, id) {
@@ -59,7 +60,8 @@ $(function(){
}); });
$('.clickable').dropzone({ $('.clickable').dropzone({
url: '@context.path/upload/wiki/@repository.owner/@repository.name', url: '@context.path/upload/wiki/@repository.owner/@repository.name',
maxFilesize: @{FileUtil.MaxFileSize / 1024 / 1024}, maxFilesize: @{context.settings.upload.maxFileSize / 1024 / 1024},
timeout: @{context.settings.upload.timeout},
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>", previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) { success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')'; var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';

View File

@@ -80,16 +80,6 @@
</cookie-config> </cookie-config>
</session-config> </session-config>
<!-- ===================================================================== -->
<!-- Optional configurations -->
<!-- ===================================================================== -->
<!--
<context-param>
<param-name>gitbucket.home</param-name>
<param-value>PATH_TO_DATADIR</param-value>
</context-param>
-->
<mime-mapping> <mime-mapping>
<extension>svg</extension> <extension>svg</extension>
<mime-type>image/svg+xml</mime-type> <mime-type>image/svg+xml</mime-type>

View File

@@ -76,12 +76,12 @@ function displayErrors(data, elem){
* @param ignoreSpace {Number} 0: include, 1: ignore * @param ignoreSpace {Number} 0: include, 1: ignore
*/ */
function diffUsingJS(oldTextId, newTextId, outputId, viewType, ignoreSpace) { function diffUsingJS(oldTextId, newTextId, outputId, viewType, ignoreSpace) {
var old = $('#'+oldTextId), head = $('#' + newTextId); var old = $('#' + oldTextId), head = $('#' + newTextId);
var render = new JsDiffRender({ var render = new JsDiffRender({
oldText: old.val(), oldText : old.data('val'),
oldTextName: old.attr('data-file-name'), oldTextName: old.data('file-name'),
newText: head.val(), newText : head.data('val'),
newTextName: head.attr('data-file-name'), newTextName: head.data('file-name'),
ignoreSpace: ignoreSpace, ignoreSpace: ignoreSpace,
contextSize: 4 contextSize: 4
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -110,7 +110,7 @@ table.diff ins{
background-color: #a6f3a6; background-color: #a6f3a6;
text-decoration: none; text-decoration: none;
} }
table.diff del{ table.diff .body.delete del{
background-color: #f8cbcb; background-color: #f8cbcb;
text-decoration: none; text-decoration: none;
} }

View File

@@ -520,6 +520,7 @@ object ApiSpecModels {
|"user":$jsonUser, |"user":$jsonUser,
|"labels":[$jsonLabel], |"labels":[$jsonLabel],
|"assignee":$jsonUser, |"assignee":$jsonUser,
|"draft":true,
|"id":0, |"id":0,
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347", |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347",
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347",
@@ -706,7 +707,7 @@ object ApiSpecModels {
|"size":100, |"size":100,
|"label":"release.zip", |"label":"release.zip",
|"file_id":"${assetFileName}", |"file_id":"${assetFileName}",
|"browser_download_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/releases/tag1/assets/${assetFileName}" |"browser_download_url":"http://gitbucket.exmple.com/octocat/Hello-World/releases/tag1/assets/${assetFileName}"
|}""".stripMargin |}""".stripMargin
val jsonRelease = val jsonRelease =

View File

@@ -14,7 +14,7 @@ import scala.util.Using
class MergeServiceSpec extends FunSpec { class MergeServiceSpec extends FunSpec {
val service = new MergeService with AccountService with ActivityService with IssuesService with LabelsService val service = new MergeService with AccountService with ActivityService with IssuesService with LabelsService
with MilestonesService with RepositoryService with PrioritiesService with PullRequestService with CommitsService with MilestonesService with RepositoryService with PrioritiesService with PullRequestService with CommitsService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService {} with WebHookPullRequestService with WebHookPullRequestReviewCommentService with RequestCache {}
val branch = "master" val branch = "master"
val issueId = 10 val issueId = 10
def initRepository(owner: String, name: String): File = { def initRepository(owner: String, name: String): File = {

View File

@@ -18,7 +18,8 @@ class PullRequestServiceSpec
with PrioritiesService with PrioritiesService
with WebHookService with WebHookService
with WebHookPullRequestService with WebHookPullRequestService
with WebHookPullRequestReviewCommentService { with WebHookPullRequestReviewCommentService
with RequestCache {
def swap(r: (Issue, PullRequest)) = (r._2 -> r._1) def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)

View File

@@ -12,7 +12,7 @@ import java.sql.DriverManager
import java.io.File import java.io.File
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.service.SystemSettingsService.{Ssh, SystemSettings} import gitbucket.core.service.SystemSettingsService.{RepositoryOperation, Ssh, SystemSettings}
import javax.servlet.http.{HttpServletRequest, HttpSession} import javax.servlet.http.{HttpServletRequest, HttpSession}
import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._ import org.mockito.Mockito._
@@ -36,9 +36,17 @@ trait ServiceSpecBase extends MockitoSugar {
allowAccountRegistration = false, allowAccountRegistration = false,
allowAnonymousAccess = true, allowAnonymousAccess = true,
isCreateRepoOptionPublic = true, isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
),
gravatar = false, gravatar = false,
notification = false, notification = false,
activityLogLimit = None, activityLogLimit = None,
limitVisibleRepositories = false,
ssh = Ssh( ssh = Ssh(
enabled = false, enabled = false,
sshHost = None, sshHost = None,
@@ -51,10 +59,17 @@ trait ServiceSpecBase extends MockitoSugar {
oidcAuthentication = false, oidcAuthentication = false,
oidc = None, oidc = None,
skinName = "skin-blue", skinName = "skin-blue",
userDefinedCss = None,
showMailAddress = false, showMailAddress = false,
webHook = SystemSettingsService.WebHook( webHook = SystemSettingsService.WebHook(
blockPrivateAddress = false, blockPrivateAddress = false,
whitelist = Nil whitelist = Nil
),
upload = SystemSettingsService.Upload(
maxFileSize = 3 * 1024 * 1024,
timeout = 30 * 10000,
largeMaxFileSize = 3 * 1024 * 1024,
largeTimeout = 30 * 10000
) )
) )
@@ -84,7 +99,7 @@ trait ServiceSpecBase extends MockitoSugar {
lazy val dummyService = new RepositoryService with AccountService with ActivityService with IssuesService lazy val dummyService = new RepositoryService with AccountService with ActivityService with IssuesService
with MergeService with PullRequestService with CommitsService with CommitStatusService with LabelsService with MergeService with PullRequestService with CommitsService with CommitStatusService with LabelsService
with MilestonesService with PrioritiesService with WebHookService with WebHookPullRequestService with MilestonesService with PrioritiesService with WebHookService with WebHookPullRequestService
with WebHookPullRequestReviewCommentService { with WebHookPullRequestReviewCommentService with RequestCache {
override def fetchAsPullRequest( override def fetchAsPullRequest(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
@@ -107,7 +122,8 @@ trait ServiceSpecBase extends MockitoSugar {
} }
def generateNewIssue(userName: String, repositoryName: String, loginUser: String = "root")( def generateNewIssue(userName: String, repositoryName: String, loginUser: String = "root")(
implicit s: Session implicit
s: Session
): Int = { ): Int = {
dummyService.insertIssue( dummyService.insertIssue(
owner = userName, owner = userName,
@@ -123,7 +139,8 @@ trait ServiceSpecBase extends MockitoSugar {
} }
def generateNewPullRequest(base: String, request: String, loginUser: String)( def generateNewPullRequest(base: String, request: String, loginUser: String)(
implicit s: Session implicit
s: Session
): (Issue, PullRequest) = { ): (Issue, PullRequest) = {
implicit val context = Context(createSystemSettings(), None, this.request) implicit val context = Context(createSystemSettings(), None, this.request)
val Array(baseUserName, baseRepositoryName, baesBranch) = base.split("/") val Array(baseUserName, baseRepositoryName, baesBranch) = base.split("/")

View File

@@ -7,7 +7,7 @@ import gitbucket.core.model.WebHookContentType
class WebHookServiceSpec extends FunSuite with ServiceSpecBase { class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
lazy val service = new WebHookPullRequestService with AccountService with ActivityService with RepositoryService lazy val service = new WebHookPullRequestService with AccountService with ActivityService with RepositoryService
with MergeService with PullRequestService with IssuesService with CommitsService with LabelsService with MergeService with PullRequestService with IssuesService with CommitsService with LabelsService
with MilestonesService with PrioritiesService with WebHookPullRequestReviewCommentService with MilestonesService with PrioritiesService with WebHookPullRequestReviewCommentService with RequestCache
test("WebHookPullRequestService.getPullRequestsByRequestForWebhook") { test("WebHookPullRequestService.getPullRequestsByRequestForWebhook") {
withTestDB { implicit session => withTestDB { implicit session =>

View File

@@ -7,7 +7,7 @@ import javax.servlet.http.{HttpServletRequest, HttpSession}
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.RequestCache import gitbucket.core.service.RequestCache
import gitbucket.core.service.SystemSettingsService.{Ssh, SystemSettings, WebHook} import gitbucket.core.service.SystemSettingsService.{RepositoryOperation, Ssh, SystemSettings, Upload, WebHook}
import org.mockito.Mockito._ import org.mockito.Mockito._
import org.scalatest.FunSpec import org.scalatest.FunSpec
import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.mockito.MockitoSugar
@@ -122,9 +122,17 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
allowAccountRegistration = false, allowAccountRegistration = false,
allowAnonymousAccess = true, allowAnonymousAccess = true,
isCreateRepoOptionPublic = true, isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
),
gravatar = useGravatar, gravatar = useGravatar,
notification = false, notification = false,
activityLogLimit = None, activityLogLimit = None,
limitVisibleRepositories = false,
ssh = Ssh( ssh = Ssh(
enabled = false, enabled = false,
sshHost = None, sshHost = None,
@@ -137,10 +145,17 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
oidcAuthentication = false, oidcAuthentication = false,
oidc = None, oidc = None,
skinName = "skin-blue", skinName = "skin-blue",
userDefinedCss = None,
showMailAddress = false, showMailAddress = false,
webHook = WebHook( webHook = WebHook(
blockPrivateAddress = false, blockPrivateAddress = false,
whitelist = Nil whitelist = Nil
),
upload = Upload(
maxFileSize = 3 * 1024 * 1024,
timeout = 30 * 10000,
largeMaxFileSize = 3 * 1024 * 1024,
largeTimeout = 30 * 10000
) )
) )
@@ -150,11 +165,13 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
class AvatarImageProviderImpl(account: Option[Account]) extends AvatarImageProvider with RequestCache { class AvatarImageProviderImpl(account: Option[Account]) extends AvatarImageProvider with RequestCache {
def toHtml(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)( def toHtml(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(
implicit context: Context implicit
context: Context
): Html = getAvatarImageHtml(userName, size, mailAddress, tooltip) ): Html = getAvatarImageHtml(userName, size, mailAddress, tooltip)
override def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = account override def getAccountByMailAddressFromCache(mailAddress: String)(implicit context: Context): Option[Account] =
override def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = account account
override def getAccountByUserNameFromCache(userName: String)(implicit context: Context): Option[Account] = account
} }
} }